This section contains miscellaneous recipes for solving problems in
docassemble.
Require a checkbox to be checked
Using validation code
Using datatype: checkboxes
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.
You could also use Redis to store the status of an interview.
Exit interview with a hyperlink rather than a redirect
Suppose you have a final screen in your interview that looks like this:
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:
Helper functions defined in a module file can be useful for inserting
complex HTML into your question blocks without making the question
blocks less readable.
In the docassemble.demo package, there is a module,
docassemble.demo.accordion, which demonstrates a set of functions,
start_accordion(), next_accordion(), and end_accordion(), that
return HTML.
When using functions like these that change the HTML structure of the
screen, it is very important not to forget to call the functions that
insert closing HTML tags, like end_accordion() in this example. If
the correct functions are not called, the HTML of the screen could be
invalid.
docassemble add-on packages could be created that offer user
interface enhancements invoked through functions. In the examples
above, the functionality was imported through a modules block, but
it would also be possible to instruct users of an add-on package to
use an include block to activate the functionality. The include
block could point to a file in the questions folder of the add-on
package that contains a modules block that imports the functions, as
well as a features block that activates custom JavaScript and CSS.
Displaying cards
Bootstrap has a component called a Card that puts text in a box
with rounded corners. Here is an example of an add-on utility that
facilitates the use of the Card.
The module defines two functions, card_start() and card_end(),
which are used to mark the beginning and end of a Card. The two
functions return HTML. The text that you want to appear in the Card
is written in Markdown format in between the call to card_start()
and the call to card_end(). If you forget to include card_end(),
there will be an HTML error on the screen.
Note that the card_start() function makes use of the Markdown in
HTML extension. Using markdown="span" enables the parsing of
Markdown in the interior of the <div>. Otherwise, any Markdown
formatting in the body of the Card would be presented literally on
the screen.
To use this module in your own interviews, save cards.yml and
cards.py to your package and modify them as you wish. Since
cards.yml only has a single modules block, so you might be
tempted to do away with it and simply include cards.py directly in
interviews that need to use the Card UI. However, using a YAML file
makes sense because you may wish to format Card elements with custom
CSS classes. In that case, you can add a features block to your
cards.yml file, and any interviews that include cards.yml will
not need to be modified.
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.
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().
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.
The above interview requires setting multi_user = True. To avoid
this you can use the following pair of interviews.
First interview:
Second interview (referenced in the first as second-interview.yml):
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.
Here
is a more complex version that handles multiple documents in Word
or PDF format and integrates with the Legal Server case management
system. It requires login and expects the Configuration to contain
the Legal Server domain name in the directive legal server domain.
Validating uploaded files
Here is an interview that makes the user upload a different file if
the file the user uploads is too large.
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.
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.
In your interview, include idle.js in a features block and
include an event: log_user_out block that executes
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.
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.
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.
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.
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.
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. Also, the example above only
works if the Configuration contains restrict input variables: false.
Running actions with Ajax
Here is an example of using JavaScript to run an action using Ajax.
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:
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:
Create an interview YAML file (called, e.g., teststripe.yml) with
the following contents:
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:
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
docassembleAPI 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.
Create an AWS Lambda function and trigger it with an HTTP REST API
that is authenticated with an API key.
Add a layer that provides the requests module. Then write a
function like the following.
Set the environment variables so that your provide the function with
an API key for the docassembleAPI (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).
Go to form.io and create a form that looks like this:
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.
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.
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.
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.
When the variable counter is incremented by the new_versionaction, 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.
For security purposes, this example interview requires admin or
developer privileges. You can find the source on GitHub and try it
out on your own server.
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.
This interview sends a document out for signature to an arbitrary
number of signers, and then e-mails the document to the signers
when all signatures have been provided.
This interview uses include to bring in the contents of
docassemble.demo:data/questions/sign.yml. This YAML file
disables server-side encryption by setting multi_user to True,
and loads the SigningProcess class from the
docassemble.demo.sign module. The object sign, an instance of
SigningProcess, controls the gathering and display of signatures.
Note the way signatures and the dates of signatures are included in
the DOCX template file, declaration_of_favorite_fruit.docx:
The template uses methods on the sign object to include the
signature images and dates. To include a signature of an Individual
or Person called the_person, you would call
sign.signature_of(the_person). To include the date when the person
signed, you would call sign.signature_date_of(the_person).
Note also the way that a line is placed underneath the signature
image: on the line following the reference to
sign.signature_of(user), there is an empty table with a top
border and no other borders.
The interview gathers the user’s name, the user’s e-mail address, and
the names and e-mail addresses of one or more witnesses.
When the interview logic calls sign.out_for_signature(), this
triggers the following process:
In a background process, e-mails are sent to the user and the
witnesses with a hyperlink they can click on to sign the document.
The hyperlink was created by interview_url_action() using the
action name sign.request_signature and an action argument
called code that contains a special code that identifies who the
signer is.
When the signer clicks the hyperlink, they join the interview
session and the action is run, and the action calls
force_ask() to set up a series of screens that signer should see.
First the signer agrees to sign, then the signer signs, and then
the signer sees a “thank you” screen on which the signer can
download the document containing their signature.
When all of the signatures have been collected, e-mails are sent to
the signers attaching the final signed document.
The sign.signature_of(the_person) method is smart; it will return the empty
string '' if the_person has not signed yet, and it will return the
person’s signature if the_person has signed. Similarly,
sign.signature_date_of(the_person) returns a line of underscores if
the person has not signed yet, and otherwise returns a DADateTime
object containing the date when the person signed. Specifically, when
the person has not signed yet, the methods return a DAEmpty
object. This means you can safely write
sign.signature_of(the_person).show(width='1in') or
sign.signature_date_of(the_person).format('yyyy-MM-dd') and the
method will still return the empty string or a line of underscores.
In this example, only one document, statement, is assembled. When
you initialize a SigningProcess object, you could specify more than
one document. For example, instead of documents='statement', you
could write documents=['statement', 'certificate_of_service']. Then
the system would send out two documents for signature.
Note that documents refers not to the actual variables statement
and certificate_of_service, but to the variables as text:
'statement' and 'certificate_of_service'. This is important; if
you were to write documents=statement or documents=[statement,
certificate_of_service], you would get an error. It is necessary to
refer to the documents using text references because the document
itself contains references to sign, and if sign could not be
defined until statement was defined, that would be a Catch-22.
The SigningProcess object knows who the signers are because of the
calls to sign.signature_of() and sign.signature_date_of() that
were made while the document was being assembled. Therefore, you
don’t have to explicitly state who needs to sign the document; you can
use logic in your document to determine who the signers are.
Note that in the attachment code part of the final question, it
refers to sign.list_of_documents(refresh=True) instead of
statement. This is important because the underlying document is
constantly changing as the signatures are added. Calling
sign.list_of_documents(refresh=True) will re-assemble the document
and then return [statement] (the statement document inside a
Python list). The list_of_documents() method simply returns a list of
DAFileCollection objects.
The signing process is customizable. The sign.yml file, which is
included at the top of the interview YAML file above, contains
default template and question blocks that you can override in
your own YAML. For example, the default content for the initial
e-mail to a signer is:
You can overwrite this by including the following rule in your own
YAML, which will take precedence over the above rule.
For more information about what templates are used, see the
sign.yml file. Some of the blocks in this file define actions;
do not try to modify these unless you are sure you know what you are
doing.
The methods of the SigningProcess object that you might use are as
follows.
sign.out_for_signature() initiates the signing process. It is safe
to run this more than once. If the signing process has already been
started, the method will not do anything.
sign.signer(i) is usually invoked in a template in a context where
i is a code that uniquely identifies the signer. It returns the
signer as an object.
sign.url(i) is also used inside of template blocks. It returns
the link that a signer should click on in order to join the interview
and sign the document or documents.
sign.list_of_documents() returns the assembled documents as a list
of DAFileCollection objects. If the optional keyword parameter
refresh is set to True, then the documents will be re-assembled
before they are returned.
sign.number_of_documents() returns the number of documents as an
integer.
sign.singular_or_plural() is useful when writing templates. The
first argument is what should be returned if there is one document.
The second argument is what should be returned if there are multiple
documents.
sign.documents_name() will return a comma_and_list() of the names
of the documents.
sign.has_signed(the_signer) will return True if the_signer has
signed the document yet, and otherwise will return False.
sign.all_signatures_in() will return True if all of the signers
have signed, and otherwise will return False.
sign.list_of_signers() will return a DAList() of the signers.
sign.number_of_signers() will return the number of signers as an
integer. sign.signers_who_signed() will return the subset who have
signed, and sign.signers_who_did_not_sign() will return the subset
who have not yet signed.
The methods sign.signature_of(the_signer) and
sign.signature_date_of(the_signer) are discussed above. These are
typically used inside of documents.
There is also the method sign.signature_datetime_of(the_signer),
which returns a DADateTime object representing the exact time the
signer’s signature was recorded. There is also the method
sign.signature_ip_address_of(the_signer), which returns the IP
address the signer was using. The IP address, along with the date and
time, are widely used ingredients of digital signatures.
sign.sign_for(the_signer, the_signer.signature) will insert a
signature manually, where the_signer is a person whose signature is
referenced in the document, using the signature_of() method, and
the_signer.signature is a variable defined by a signature block
(a DAFile object). Note that if this method is called more than
once, each new time the date of the signature will be updated. You
may want to avoid this by doing:
Then this code can safely be called more than once without changing
the date of the signature.
Calling sign.refresh_documents() will generate fresh copies of the
documents. (It uses the reconsider() function.)
Note that in the sign.yml file, the signature block uses
validation code that calls x.validate_signature(i). This is
important because it performs the additional tasks necessary to record
the signature, such as recording the IP address. You will probably
not need to call validate_signature() outside of this context; the
sign_for() method calls validate_signature() for you.
Validating international phone numbers
Here is a simple way to validate international phone numbers.
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.
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.
Passing variables from one interview session to another
Using create_session(), set_session_variables(), and
interview_url(), you can start a user in one session, collect
information, and then initiate a new session, write variables into the
interview answers of that session, and direct the user to that session.
Note that when the user starts the session in the second interview,
the interview already knows the object user and already knows the
value of favorite_fruit.
Making generic questions customizable
If you have a lot of questions in your interviews that are very
similar, such as questions that ask for names and addresses, you might
want to create a YAML file that contains generic object
questions and then include that YAML file in all of your interviews.
This is a way to ensure consistency across your interviews without
having to maintain the same information in multiple places across your
YAML files.
Having a common set of generic object questions does not inhibit
your ability to customize question blocks when you need to; you
can always override the generic object question with a more
specific question if you would like to ask a question a different way
if you have a special case.
It is also possible to customize your generic object questions
without overriding. This recipe demonstrates a method of designing
generic object questions that allows for the customization of
specific details of questions.
Here is an example of an interview that gathers names, e-mail
addresses, and addresses of three Individuals while relying
entirely on generic object questions to gather the information.
Note that all of the blocks in these YAML files are generic object
blocks. There are question blocks that define questions to be
used. These question blocks make reference to a lot of different
object attributes that function as “settings.” After the question
blocks, there are template blocks and code blocks that
set default values for these settings.
This means that in your own interviews, you have the option of
overriding any of those settings simply by including a block that
sets a value for one of the settings. For example, the default value
of the .ask_about_homelessness attriute is False, but in the
example interview, this was overridden for the object client:
Using templates to specify the question and subquestion
text, using the subject part and the content part of the
template. Alternatively, you could use separate templates for the
question and subquestion, so that your interviews could override
the question part without overriding the subquestion part, and
vice-versa.
Specifying multiple question blocks and using the if
modifier to choose which one is applicable, depending on the values
of “settings.”
Using the code variant of show if to select or deselect fields
in a list of fields, depending on the values of settings.
This example uses the [TARGET]feature
to show a document assembly preview in the right portion of the
screen that continually updates as the user enters information into
fields.
Using a spreadsheet as a database for looking up information
If you have a table of information and you want to be able to look
things up in that table of information during your interview, one of
the ways you can accomplish this is by keeping the information in a
spreadsheet in the Source folder of your package. Python can be used
to read the spreadsheet and look up information in it.
The fruit_database module uses the pandas module to read an
XLSX spreadsheet file into memory. The get_fruit_names() function
returns a list of fruits (from the “Name” column) and the
fruit_info() function returns a dictionary of information about a
given fruit.
Note that the interview does not simply import the
fruit_info_by_name dictionary into the interview answers. Although
technically you can import dictionaries, lists, and other Python data
types into your interview, doing so is generally not a good idea
because those values will then become part of the interview answers
that are stored in the database for every step of the interview. This
wastes hard drive space and it also wastes time re-loading the data
out of your module every time the screen loads. It is a best practice
to use helper functions like get_fruit_names() and fruit_info() to
bring in information on an as-needed basis.
If your database is particularly large, reading it into memory may not
be a good idea. You might want to rewrite the functions so that they
read information out of the file on an as-needed basis. You also
might want to adapt the functions to read data from a Google Sheet or
Airtable rather than from an Excel spreadsheet that is part of your package.
Using a template that lives on Google Drive
When you use the docx template file or pdf template file
features to assemble documents, your document templates are typically
located in the Templates folder of your package, and you refer to
templates by their file names. However, the docx template file or
pdf template file specifiers can be givencode instead of a
filename. Your template could be a DAFile object.
You can use this feature to retrieve templates from Google Drive.
There is a convenient Python package called googledrivedownloader
that allows you to download a file from Google Drive based on its ID.
The following example demonstrates how to use this in a document
assembly interview:
It is necessary that the sharing settings on the file in Google Drive
be set so that “anyone with the link” can view the file. In this
example, the Google Drive file is a DOCX file with this sharing
link. The
ID of the file on Google Drive is
'1MsiAA632Ehyj0jEW_WEBpZOd-qokqyni'. This ID can be seen inside of
the URL, before the query part (the part beginning with ?).
If you wish to use a file on Google Drive without creating a link that
anyone in the world can view, you can use the DAGoogleAPI object,
which allows you to use Google’s API with service account
credentials that are stored in the Configuration under service
account credentials within the google directive.
This example uses a Google Drive file with the id of
'1IGvzA-oOB_bVTmDB7TPW9UgQT-6MGtSe'. This file is not accessible
via a link; it is shared only with the special e-mail address of the
service account.
This interview uses a Python module docassemble.demo.gd_fetch,
which provides a function called fetch_file(), which downloads a
file from Google Drive.
The fetch_file() function retrieves the file with the given
file_id from Google Drive, using Google’s API, and then saves the
contents of the file to path, which is a file path.
The Python module apiclient provides the interface to Google’s API.
apiclient is the name of the module contained in the
google-api-python-client package. The fetch_file() function
uses a DAGoogleAPI object to get authentication credentials from
the Configuration. All that .drive_service() does is:
On the server demo.docassemble.org, the Configuration contains
credentials for a Google service account, and the Google Drive file
'1IGvzA-oOB_bVTmDB7TPW9UgQT-6MGtSe' has been shared with that
service account.
Although it is convenient for document templates to be stored in
Google Drive instead of inside of a package, keep in mind that there
are risks to storing templates on Google Drive. If you have a
production interview that uses your template from Google Drive, and
you edit the template in such a way that it causes an error (for
example, a Jinja2 syntax error), your users will start seeing errors
immediately. It would be better if you only released a new version of
a template as part of a process that involves testing and validating
your changes to a template. Another issue is that you are opening up
a back door for code injection. If someone has edit access to the
template on Google Drive, they could potentially find a way to use
Jinja2 to execute arbitrary Python code on your server. Templates are
a form of software, so ideally they should live where the other
software lives, which is inside of a software package.
Changing the language using the navigation bar
This example interview provides the user with a language selector
interface in the navigation bar. It uses default screen parts to
set the navigation bar html, which is a screen part inside of a
<ul class="nav navbar-nav"> element in the navigation bar.
Note that default screen parts is used instead of metadata so
that Mako can be used. The url_of() function is used to insert
the URL of an image into the src attribute of an <img> element.
The action that changes the language uses set_save_status() to
prevent language switches from introducing a step in the interview
process.
For more information about docassemble’s support for multi-lingual
interviews, see the Language Support section.
Tracking statistics about user activity
Redis has a convenient feature for incrementing a counter. Since the
data in Redis is not affected by the deletion of interview sessions
or users pressing the Back button, Redis can be used to keep track
of event counters. The following interview uses a DARedis object
to access Redis and increment counters that track how many times
particular screens in an interview were reached.
When you retrieve data from Redis, it comes through as non-decoded
binary data, so you have to decode it with .decode().
Note that r.incr() was not called inside the
mandatorycode block. If it had been, then the
counters for the early parts of the interview would be repeatedly
incremented every time the screen loaded. Using the milestone
dictionary to track whether the milestone has been reached ensures
that the counters are not incremented duplicatively.
Headless document assembly
If you want to use the document assembly features of docassemble
without the user interface, you can use the API to drive a “headless”
interview.
For example, you could use an interview like this:
Then you could drive the interview with the API using code like this:
Building a database for reporting
If you want to be able to run reports on variables in interview
sessions, calling interview_list() may be too inefficient because
unpickling the interview answers of a large quantity of interview
sessions is computationally intensive.
The store_variables_snapshot() function allows you to save
variable values to a dictionary in a PostgreSQL database. This
database can be the the standard database where interview answers are
stored (db) or a different database (variables snapshot
db). You can then write queries that use JSONB to access the
variables in the dictionary.
Here is an example of a module, index.py, that uses
store_variables_snapshot() to create a database that lets you run
reports showing the names of users along with when they started and
stopped their interviews.
Here is an example of an interview that uses a MyIndex object to
store information about each interview in JSON storage.
Note that the index object is defined in a mandatory block. This
ensures that the index object exists before the on change code
runs. Code that runs inside of on change cannot encounter any
undefined variables.
To run a query on the data, you can do something like this:
docassemble is a Flask application, which means that web
endpoints can be added simply by declaring a function that uses the
Flaskroute decorator.
Here is an example of a Python module that, when installed on a
docassemble server, enables /hello as a GET endpoint.
This example uses the base_templates/base.html Jinja2 template,
which is the default template for pages in docassemble. Using this
template allows you to create a page that uses the same look-and-feel
and the same metadata as other pages of the docassemble app. Note
that the keyword arguments to render_template_string() define
variables that the base_templates/base.html uses. You can customize
different parts of the page by setting these values. The exception is
planet, which is a variable that is used in the HTML for the
/hello page. Note that in order to insert raw HTML using keyword
parameters, you need to use the Markup() function.
Flask only permits one template folder, and the template folder in
the docassemble app is the one in docassemble.webapp. This
cannot be changed. However, you can provide a complete HTML page to
render_template_string() if you do not want to use a template.
The line # pre-load at the top of the module is important. This
ensures that the module will be loaded when the server starts.
The root endpoint / already has a definition in
docassemble.webapp.server, but you can tell the server to redirect
requests from / to a custom endpoint that you create.
You might want to use this technique to host your own web site on
various endpoints of the docassemble server and then incorporate
docassemble interviews using a <div> or an <iframe>. This
avoids problems with CORS that might otherwise interfere with
embedding.
Building a custom API endpoint with Flask
Much as you can create a custom page in the web application using
Flask, you can create a custom API endpoint.
Here is an example of a Python module that, when installed on a
docassemble server, enables /create_prepopulate as a POST
endpoint. This endpoint creates a session in an interview indicated by
the URL parameter i, and then prepopulates variables in the
interview answers using the POST data. This might be useful in a
situation where you want to combine multiple API calls into one.
The api_verify() function handles authentication using
docassemble’s API key system, and it logs in the owner of the API
key, so that subsequent Python code will run with the permissions of
that user. Note that the code in an API endpoint does not run in the
context of a docassemble interview session, so there are many
functions that you cannot call because they depend on that context
The POST data may be in application/json or
application/x-www-form-urlencoded format.
Running background tasks from endpoints
docassemble has a background tasks system that can be called
from inside of interview logic. The background_action() function
cannot be called from a custom endpoint, however, because it
depends upon the interview logic context.
In order to run background tasks from a custom endpoint, you need to
interface with Celery directly.
First, you need to create a .py file that defines Celery task
functions. In this example, the file is custombg.py in the
docassemble.mypackage package:
The first line, # do not pre-load, is important. This file should
not be loaded as an ordinary Python module. Instead, it should be
loaded using the celery modules directive:
The celery modules directive ensures that the module will be
loaded at the correct time and in the correct context.
Then create a second Python file containing the code for your Flask
endpoints. The following file is testcustombg.py in the
docassemble.mypackage package.
To prevent a circularity in module loading, it is important to refrain
from importing the background task module,
docassemble.mypackage.custombg, into this module if in_celery is
true (meaning that Celery rather than the web application is loading
the docassemble.mypackage.custombg module). Although this creates a
situation where custom_add_four and custom_comma_and_list are
undefined when in_celery is true, this does not matter because the
code for your endpoints will never be called by Celery.
The custom_add_four and custom_comma_and_list functions are called
in the standard Celery fashion. See the Celery documentation for
more information about using Celery.
The testcustombg.py file above demonstrates how you can create
separate API endpoints for starting a long-running process and polling
for its result.
Synchronizing screen parts with interview answers
If you run multiple sessions in the same interview and you want to be
able to keep sessions organized on the My Interviews page, you might
want to use set_parts() to dynamically change the interview title
or subtitle. You can use metadata to set default values of title
or subtitle that will apply when the user first starts the
interview. Then after the user answers certain questions, you can call
set_parts() to change the title or subtitle. That way, when you
go to the My Interviews page, you will be able to tell your sessions
apart.
This example uses on change to trigger calls to set_parts()
when a variable changes. Although you could also call set_parts()
in your ordinary interview logic, using on change is helpful because
if you allow the user to make changes to variables in a review
screen, you don’t need to worry about making sure that set_parts()
gets called again.
If you are using the Markdown-to-PDF document assembly method and you
want the resulting PDF documents to bear a watermark on each page,
you can use the draftwatermark package in LaTeX to place an
image in the center of each page. This package is not enabled by
default in the default LaTeX template or its default metadata, so
you need to tell docassemble to load it in the preamble of the
.tex file. The default LaTeX template allows you to add your own
lines to the preamble of the .tex file by setting the
header-includes metadata variable to a list of lines.
This interview uses the module file calendar.py, which imports
the ics package.
Duplicating a session
This example shows to use create_session(),
set_session_variables(), and all_variables() to create a new
session pre-populated with variables from the current session.
Note that there are hidden variables (stored in the _internal
variable) that pertain to the session that can be carried
over. Copying over all of them is usually not a good idea, but some of
them can be transferred. This example copies answered and answers,
which keep track of which mandatory blocks have been completed and
what the answers to multiple-choice
questions
are. The device_local and user_local hidden variables are also
copied. The starttime and referer variables are not copied over,
but they could be. This example does not copy over data about any
current “actions” in progress (event_stack).
A table in a DOCX file that uses a list nested in a dictionary
This example illustrates how to use a docx template file and
Jinja2 to construct a table in a DOCX file that contains a list
nested in a dictionary, with a total and subtotals. The template file
is nested_list_table.docx.
Generating a graph and inserting it into a document
This interview uses the matplotlib library (which is not installed
by default on a docassemble server) to generate a pie chart based
on user-supplied input.
The bulk of the work is done in the graph.py module, the contents
of which are as follows.
The document template, graph.docx, inserts the DAFile object graph.
Restyling checkboxes as buttons
CSS is a very powerful tool for customizing the user interface. Here
is an example that replaces docassemble’s standard checkboxes with
buttons that are grey when unselected and red when selected
If you have a PDF form that only allows a few lines for a list of
things, you can conditionally generate a continuation page. Here is
one way to do it.
If you allow your users to “edit” a file upload by sending them back
to the question with the datatype: file or datatype: files
field, the only way they can “edit” the upload is by re-uploading a
new file or a new set of files. The value of the DAFileList is
simply replaced. This is because datatype: files and datatype:
file produce an <input type="file"> HTML element, which is
incapable of having a default value.
To allow the user to edit a file upload, you can instead send them to
a question that lets them see the files they have uploaded, delete
particular ones, reorder the files, and add additional files.
In this example, the file upload-handler.yml defines rules that
apply to any DAFileList (the blocks use generic object:
DAFileList). The complicated part is the validation code on the
question that sets x[i]. Without this, the DAFileListx would
be a list of DAFileList objects rather than a list of DAFile
objects.
Note that the interview requires a definition of
exhibits.verified. This is important; if your interview doesn’t have
logic that seeks out the .verified attribute, the user will not see
the screens that allow them to edit the list of uploaded files.
This recipe requires docassemble version 1.4.73 or higher.
Using js show if with yesnomaybe
Here is an example of using js show if with datatype: yesnomaybe.
A subclass of Individual that reduces to text differently depending on the context
Normally, when you reduce an object of class Individual to text, the
result is the same as when you call .name.full() on the
object. You can make a subclass of Individual that reduces to text
in a different way.
Here is an example module that uses current_context().inside_of to
detect whether the object representing an individual is being reduced
to text inside of a document that is being assembled. If it is, the
.name_full() name is returned, but if the object is being reduced to
text for purposes of displaying in the web app, the first name is
returned.
Here is an interview that demonstrates the use of the AltIndividual
class.
You can control access to interviews using the username/password
system, and you can use the require login and required
privileges.
If you already have a username/password system on another
web site, and you don’t want users to have to log in twice, there are
ways around this:
If both your other site and your docassemble side use the same
social sign-on system, like Azure, the login process can be fairly
transparent.
Your other site could synchronize the usernames and passwords
between your other site and the docassemble site. The
docassemble API allows for creating users, deleting users, and
changing their passwords.
Your users can use the docassemble site without logging in to
the docassemble site. Your interviews can control access and
identify the user through a different means.
To make an interview non-public, you can put something like this at the top of the interview:
If a random person on the internet tries to access the docassemble
interview through a URL, they will be immediately redirected. A new
session will be created, but it will immediately be deleted by
the operation of command('exit').
Meanwhile, users of your web site will be able to use interview
sessions that your site creates for them. Your site can call the
following API endpoints:
interview with the filename i. A session ID is returned.
interview answers of a session identified by i and session. For
example, using the session ID it obtained by calling the previous
endpoint, your site could call the /api/session POST endpoint with
variables set to {'authorized': true}.
Now you can direct the user to the URL
https://da.example.com/interview?i=docassemble.mypackage:data/questions/myinterview.yml&session=afj32vnf23wjfhf2393d928j39d8j
or, equivalently, https://da.example.com/run/mypackage/myinterview/?session=afj32vnf23wjfhf2393d928j39d8j.
The session that the user resumes will have the variable authorized
set to True and thus the user will not be redirected away. The
session code is the security mechanism.
At the same time that your site defines authorized as True, it
could also define variables such as the user’s email address, or a
unique ID for the user’s account on your site. These might be useful
to have in an interview.
There are other strategies for authentication without login. For
example, you could use to use the API to stash data in
docassemble, and then direct the user to an interview with a link
like
https://da.example.com/start/mypackage/myinterview/?k=AFJI23FJOIJ239FASD2&s=IRUWJR2389EFIJW2333
where AFJI23FJOIJ239FASD2 and IRUWJR2389EFIJW2333 are the
stash_key and the secret returned by the /api/stash_data
endpoint. Your interview logic could retrieve these values from the
url_args and then use retrieve_stashed_data() to obtain the
data.
The interview can commence only if k and s are present in the URL
parameters, and only if they are valid. When you stash the data you
can set a time period after which the data will be automatically
deleted.
This method has the advantage that it does not require using
multi_user = True. However, if the user does not log in to the
docassemble site, the user will not be able to resume an encrypted
session if multi_user is not set to True.