To help you organize groups of things, docassemble offers three data structures: lists, dictionaries, and sets. These mirror the list, dict, and set data types that exist in Python.
Overview of types of data structures
Lists in Python
A “list” is a group that has a defined order. Elements are numbered with an index that starts from zero. In Python, if a list is defined as:
then fruit[0]
will return 'apple'
, fruit[1]
will return 'orange'
,
and fruit[2]
will return 'pear'
. You can try this out in a
Python interpreter:
Adding a new element to the list is called “appending” to the list.
The sorted()
function is a built-in Python function that
arranges a list in order.
In docassemble, lists are typically defined as special objects
of type DAList
, which behave much like Python lists.
Dictionaries in Python
A “dictionary” is a group of key/value pairs. By analogy with an actual dictionary, the “key” represents the word and the “value” represents the definition. In Python, if a dictionary is defined as:
then feet['dog']
will return 4
, feet['human']
will return 2
,
and feet['bird']
will return 2
. The keys are 'dog'
, 'human'
, and
'bird'
, and the values are 4
, 2
, and 2
, respectively.
The keys of a dictionary are unique. Setting feet['rabbit'] = 4
will add a new entry to the above dictionary, whereas setting
feet['dog'] = 3
will change the existing entry for 'dog'
. The
items in a dictionary are “unordered,” so if you want to loop through
them in a particular order, you will need to take special steps to
ensure the items appear in that order, such as keeping a separate
list of the keys in your desired order.
In docassemble, dictionaries are typically objects of type
DADict
, which behave much like Python dicts.
Sets in Python
A “set” is a group of unique items with no order. There is no
index or key that allows you to refer to a particular item; an item is
either in the set or is not. (A set in Python behaves much like a set
in mathematical set theory.) In Python, a set can be defined with
a statement like colors = set(['blue', 'red'])
. Adding a new item to
the set is called “adding,” not “appending.” For example:
colors.add('green')
. If you add an item to a set when the item is
already in the set, this will have no effect on the set.
In docassemble, sets are typically objects of type DASet
,
which behave much like Python sets.
Lists, dictionaries, and sets in docassemble
When you want to gather information from the user into a list,
dictionary, or set, you should use the objects DAList
, DADict
,
and DASet
(or subtypes thereof) instead of Python’s basic
list, dict, and set data types. These objects have special
attributes that help interviews find the right blocks to use to
populate the items of the group.
If you want to, you can use Python’s basic list, dict, and set
data types in your interviews; nothing will stop you – but there are
no special features to help you gather information into these data
structures using question
blocks or code
blocks.
Gathering information into lists
The following interview populates a list of fruits.
The variable fruit
is defined as a DAList
object.
An objects
block is like a code
block, except that it performs
a special purpose of defining docassemble objects. If
docassemble needs to know the definition of the variable fruit
,
it will use this block and initialize fruit
as a DAList
. (If you
are familiar with Python, you can think of this as a block that runs
fruit = DAList('fruit')
where DAList
is a Python class.)
The next block contains the end point of the interview, a screen that says how many fruits are in the list and lists them.
Since this question
is mandatory
, docassemble tries to ask
it. However, it encounters fruit.number_as_word()
, which returns
the number of items in the list (e.g., “two,” “three,” etc.). But in
order to know how many items are in the list, docassemble needs to
ask the user what those items are. So the reference to
fruit.number_as_word()
will implicitly trigger the process of asking
questions to gather the list. The reference to ${ fruit }
would
also trigger the same process, but docassemble will encounter
fruit.number_as_word()
first.
Behind the scenes, when fruit.number_as_word()
is run, and
docassemble needs the list to be gathered, it runs
fruit.gather()
, a gathering algorithm. The .gather()
method
orchestrates the gathering process by triggering the seeking of
variables necessary to gather the list.
Many things other than ${ fruit.number_as_word()
will implicitly
trigger the gathering of the fruit
list. If you iterate on fruit
,
or run a method that uses the items in the list, this will trigger
gathering. The advantage of implicit triggering is that your code can
be concise, and your interview will be parsimonious about whether to
ask questions to gather the list; if you have no code that requires
knowing the items in the list, then the gathering questions will not
be asked. If you want to be explicit about when the list-gathering
questions are asked, you can call fruit.gather()
yourself, perhaps
in a mandatory
code
block.
The gathering algorithm behaves like a lawyer interrogating a witness.
“Do you have any children?” asks the lawyer.
“Yes,” answers the witness.
“What is the name of your first child?”
“James.”
“Besides James, do you have any other children?”
“Yes”
“What is the name of your second child?”
“Charlotte.”
“Besides James and Charlotte, do you have any other children?”
“No”
The .gather()
method triggers these questions by seeking the
values of various variables:
fruit.there_are_any
: should there be any items in the list at all?fruit[i]
: the name of thei
th fruit in the list.fruit.there_is_another
: are there any more fruits that still need to be added?
First, the interview will want to know whether there are any items in
the list at all. It will seek a definition for fruit.there_are_any
.
Thus, it will ask the question, “Are there any fruit that you would
like to add to the list?”
If the answer to this is True
, the interview will seek a definition
for fruit[0]
to gather the first element. Thus, it will ask the
question “What fruit should be added to the list?”
This question
uses the index variable i
. The special
variable i
means that the question is generalized; it can be used
and re-used for any i
(0
, 1
, 2
, 3
, etc.). docassemble’s
gathering process automatically takes care of setting the variable i
to the right value before using this question
.
Assume the user enters “apples.”
Now docassemble knows the first item in the list, but it does not
know if the list is complete yet. Therefore, it will seek a
definition for fruit.there_is_another
. It will ask the question “So
far, the fruits include apples. Are there any others?”
If the answer to this is True
, the interview will seek a definition
of fruit[1]
to gather the second item in the list. It will ask,
again, “What fruit should be added to the list?” Assume the user
enters “oranges.”
Then the interview will again seek the definition of
fruit.there_is_another
. This time, if the answer is False
, then
the fruit.gather()
method will return without asking any questions,
and fruit.number_as_word()
will respond with the the number of items
in fruit
(in this case, 2). When docassemble later encounters
The fruits are ${ fruit }.
, it will attempt to reduce the variable
fruit
to text. Since the interview knows that there are no more
elements in the list, it does not need to ask any further questions.
${ fruit }
will result in apples and oranges
.
Note that the variable i
is a special variable in
docassemble. When the interview seeks a definition for
fruit[0]
, the interview will first look for a question that offers
to define fruit[0]
. If it does not find one, it will take a more
general approach and look for a question that offers to define
fruit[i]
. The question that offers to define fruit[i]
will be
reused as many times as necessary.
Since the index variable i
is a special variable, you should never
attempt to set it yourself; you will likely get a confusing error if
you try.
Nor should you ever use i
in mandatory
or initial
blocks.
The use of i
is reserved for blocks that docassemble calls upon
when it is seeking to define a variable with an index, such as
fruit[2]
, and there is no block that explicitly defines fruit[2]
.
If you use i
in a mandatory
block, you will get an error that
i
is undefined, or if i
is defined, it might be defined as a value
that makes no sense for the context in which you are using i
.
For more information on using variables like i
, see the sections on
index variables and how docassemble finds questions for variables.
Customizing the way information is gathered
The way that docassemble asks questions to populate a list like
fruit
can be customized by setting attributes of fruit
. For
example, perhaps you would prefer that the questions in the interview
go like this:
- How many fruits are there?
- What is the name of the first fruit?
- What is the name of the second fruit?
- etc.
To ask questions this way, set the .ask_number
attribute of
fruit
to True
. Also include a question that asks “How many fruits
are there?” and use fruit.target_number
as the variable set by the
question. (The .target_number
attribute is a special attribute,
like .there_is_another
.)
This example uses the using()
method to initialize the
ask_number
attribute of fruit
. Another way to initialize the
attribute would be to use a mandatory
block at the start of the
interview:
Generally, it is best to use the using()
method.
You can avoid the .there_are_any
question by setting the .minimum_number
to a value:
Gathering a list of objects
The examples above have gathered simple variables (e.g., 'apple'
,
'orange'
) into a list. You can also gather objects into a list.
You can do this by setting the .object_type
of a DAList
(or
subtype thereof) to the type of object you want the items of the list
to be.
In this example, we gather Address
objects into a DAList
by
setting the .object_type
attribute to Address
.
There are some list types that have an .object_type
by default. For
example, DAEmailRecipientList
lists have an .object_type
of
DAEmailRecipient
.
During the gathering process, docassemble only gathers the attributes necessary to display each object as text (by default). So if you do:
then the list will consist of Individual
s, and docassemble
will gather friend[i].name.first
for each item in the list. This is
because of the way that the Individual
object works: if y
is an
Individual
, then its textual representation (e.g., including
${ y }
in a Mako template, or calling str(y)
in Python code) will
run y.name.full()
, which, at a minimum, requires a definition for
y.name.first
. (See the documentation for Individual
for more
details.) Other object types behave differently. For example,
if y
is an Address
, including ${ y }
in a Mako template will
result in y.block()
, which depends on the address
, city
, and
state
attributes. If you use a plain DAObject
as the
object_type
, then no questions will be asked; this is because
the DAObject
is meant to be a “base class,” with no meaningful
attributes of its own. Thus, calling str(y)
on a plain DAObject
will simply return a name based on the variable name; no questions
will be asked.
If your interview has a list of Individual
s and uses attributes of
the Individual
s besides the name, docassemble will eventually
gather those additional attributes, but it will ask for the names
first and only when it is done asking for the names of each individual
in the list will it start asking about the other attributes. Here is
an interview that does this:
The order of the questions is:
- What is the name of your first friend?
- Do you have any other friends?
- What is the name of your second friend?
- Do you have any other friends?
- What is Fred’s favorite animal?
- What is Fred’s birthdate?
- What is Sally’s favorite animal?
- What is Sally’s birthdate?
If you would prefer that all of the
questions about each individual be asked together, you can use the
.complete_attribute
attribute to tell docassemble that an item
is not completely gathered until a particular attribute of that item
(usually .complete
) is defined. You can then write a code
block
that defines this attribute. You can use this code
block to
ensure that all the questions you want to be asked are asked during
the gathering process.
In the above example, we can accomplish this by doing
friend.complete_attribute = 'complete'
. Then we include a code
block that sets friend[i].complete = True
. This tells
docassemble that an item friend[i]
is not fully gathered until
friend[i].complete
is defined. Thus, before docassemble moves
on to the next item in a list, it will run this code
block to
completion. This code
block will cause other attributes of
friend[i]
to be defined, including .birthdate
and
.favorite_animal
. Here is what the revised interview looks like:
Now the order of questions is:
- What is the name of your first friend?
- What is Fred’s birthdate?
- What is Fred’s favorite animal?
- Do you have any other friends?
- What is the name of your second friend?
- What is Sally’s birthdate?
- What is Sally’s favorite animal?
- Do you have any other friends?
You can use any attribute you want as the complete_attribute
.
Defining a complete_attribute
simply means that instead of ensuring
that a list item is displayable (i.e., gathering the name of an
Individual
), docassemble will seek a definition of the attribute
indicated by complete_attribute
. If .birthdate
was the only
element we wanted to define during the gathering process, we could
have written friend.complete_attribute = 'birthdate'
and skipped the
code
block entirely.
You can also set complete_attribute
to a list of attribute names. In
this case, the item will be considered complete when it has a
definition for each attribute in the list of of attributes.
It is a best practice to set complete_attribute='complete'
and to
specify a code
block that sets the .complete
attribute of the list
item to True
. This will facilitate the use of a table
for editing
the list. Every time the user edits a list item in a table
, the
.complete
attribute will be undefined if complete_attribute
is
'complete'
, and then the definition of .complete
will be sought
again. Thus the “completeness” of the list item will always be
recomputed if the user changes something.
When you write your own class definitions, you can set a
default complete_attribute
that is not really an attribute, but a method
that behaves like an attribute.
In the following example, FishList
is a list of Fish
, where a
Fish
is considered “complete” for purposes of auto-gathering when
the common_name
, scales
, and species
attributes have been
defined.
Here is an interview that uses this class definition.
Gathering lists within lists
Here is an example of gathering nested lists (a list within a list within a list).
The first block defines the objects we will use.
(Note that the line breaks here are not meaningful to the syntax; Python allows you to use line breaks in this context for aesthetic reasons.)
The list person
will be a list of objects of type Individual
.
We assume that there is at least one individual in the list, so we set
minimum_number=1
. Since we want to gather more information about
each individual in the list than simply the individual’s name (the
textual representation of an Individual
), we set
complete_attribute='complete'
to indicate that an individual is not
“complete” until the attribute .complete
is defined.
We also assert here that the attribute child
for any given person in
the list of people (person[i].child
) is a list of Individual
s,
each of which will be “complete” when the .complete
attribute is
defined. The variable i
here is a special variable that is set by
docassemble during the list gathering process. (You should never
try to set i
yourself.) If docassemble wants the definition of
person[0].child
, it will set i = 0
and then define
person[0].child
by running the second line in the objects
block.
The next block specifies what it means for an Individual
item in
the person
list to be “complete.”
This says that a given Individual
in the person
list
(person[i]
) is “complete” when the person’s name is defined, when we
have gathered a list of the person;s allergies, and when we have
gathered a list of their children.
We have seen the block that defines person[i].child
for any i
.
Later on we will see the block that defines person[i].allergy
.
When docassemble wants to make a person[0]
“complete,” it will
set i = 0
and then run this Python code block. It will keep
running this block until it gets the answers it needs. First it will
ask for the person’s name, then it will go through a list gathering
process to gather the allergies, and then go through a list gathering
process to gather the children, and when there are no more children to
gather, it will define the complete
attribute by setting it to
True
. Then the person[i]
will be “complete,” and docassemble
will continue gathering the person
list.
The next block defines what it means for a child of a given
person[i]
to be complete. It is similar to the previous block,
except the interview doesn’t ask about a child’s children.
While docassemble is running person[i].child.gather()
, it will
ask questions to gather the items in person[i].child
and to make
each item, such as person[0].child[1]
(for the first person’s second
child) “complete.” Since the child
attribute is defined with
complete_attribute='complete'
, docassemble will try to make
person[0].child[1]
“complete” by seeking a definition of
person[0].child[1].complete
. There is no block in the interview
that offers to define person[0].child[1].complete
specifically, but
the code
block above offers to define
person[i].child[j].complete
for any arbitrary i
and j
. So
docassemble will set i = 0
and j = 1
, and then try running
this code
block. The Python code in this block will trigger all
the necessary questions to make the child object “complete.”
It is very important that the code in this code
block is in a
separate block from the previous code
block. Each code
block
represents a separate statement of truth. The first code
block
says what it takes to be finished asking questions about a
person[i]
, and the second code
block says what it takes to be
finished asking questions about one of that person’s children.
If you tried to merge this code with the code from the previous block,
then you might get an error about the variable j
being undefined.
If docassemble is looking to define an attribute of person[i]
,
it will define i
and then run the block that offers to define the
attribute of person[i]
. But if docassemble, in the course of
running a block that defines the attribute of person[i]
, encounters
the variable j
, it will not know what to do with that; it didn’t set
j
to anything before running the code block, so j
will be
undefined.
While Python is a “procedural” language, the way docassemble
works is more “declarative.” In most cases, your code
blocks should
be self-contained declarations about how a single variable should be
defined, even if they cause other variables to be defined as a side
effect. In this example, those single variables are
person[i].complete
and person[i].child[j].complete
. The blocks
that define these variables will be called upon at multiple times in
your interview for the specific purpose of defining
person[i].complete
or person[i].child[j].complete
.
The next block is a reusable question
.
This question
will be used for person[0]
, person[1]
, and any
other person[i]
in the person
list. If docassemble wants to
know person[1].name.first
, it will set i = 1
and then ask this
question
.
The next block is used whenever docassemble wants to know whether there are more items to be added to a list.
The .gather()
method of the DAList
class will undefine the
.there_is_another
attribute after each item is gathered, and then
re-seek a definition of .there_is_another
to figure out if more
items need to be gathered.
The next block asks whether a person in the person
list has any
children.
This is the first question
that will be asked when docassemble
runs person[i].child.gather()
.
The next question
illustrates the use of two index variables.
The use of the index variables i
and j
mean that if
docassemble wants to find a definition for
person[1].child[2].name.first
, it will set i = 1
, set j = 2
, and
then ask this question.
If you wanted to ask the question a different way for the first person in the list, you could include the following block:
Here, the index variable i
is used instead of j
. docassemble
will only try to ask this question
if the variable it seeks begins
with person[0].child
. If docassemble is looking to define
person[1].child[0].name.first
, it will disregard this question
,
because person[0].child[i].name.first
doesn’t generalize to
person[1].child[0].name.first
.
Likewise, if you wanted to ask the question a different way for the first child, you could include:
This question offers to define person[i].child[0].name.first
for any
i
.
You would never have a block that mentions j
without also
mentioning i
, and you would never have a block that mentions k
without also mentioning j
and i
. The variable i
needs to be
used for the first index variable that is generalizable, and j
needs
to be used for the second index variable that is generalizable.
Next is the block that asks whether a person has any more children.
Next we have a series of blocks relating to gathering the allergies of
people. These are similar in functionality to other blocks in this
interview, but they are different because they use the generic
object
modifier and the special variable x
, which represents the
“generic” object.
The variable x
works in a similar way to the way that index
variables like i
and j
work. If docassemble wants to define
the attribute allergy
for an object person[0]
, and the object is
of type Individual
, it can run x = person[0]
and then process the
x.allergy: DAList
line of the objects
block. Likewise, if
**docassemble wants to define person[1].child[0].allergy[3]
, it can
set x = person[1].child[0]
, set i = 3
, and then ask the
question
that defines x.allergy[i]
. By using the generic
object
feature, we save ourselves the trouble of writing separate
questions for gathering the allergies of person[i]
and
person[i].child[j]
.
Finally, we have the single mandatory
block of the interview,
which presents to the user all of the information that was gathered
during the interview.
All of the questions that are asked during the interview are triggered
by the line % for p in person:
. In order to iterate through
person
, person
first needs to be defined. That triggers the use
of the first objects
block to define person
. Then person
needs to be gathered, because you can’t iterate through a list that
hasn’t been gathered yet. That causes docassemble to gather the
items in person
and to make them “complete.” Before any given
person[i]
can be “complete,” the person’s name needs to be
collected, the allergies need to be gathered, and the children need to
be gathered. Before a child can be “complete,” the child’s
allergies need to be gathered. All of these questions are triggered
because each time the screen loads, docassemble tries to show the
mandatory
question, and each time, it keeps encountering % for p in
person:
.
Once person
is gathered, the “for” loops all have enough
information, so no further questions needs to be asked.
Note that while the % for
, % endfor
, % if
, and % endif
lines
are indented when nested, the actual lines of text are not indented.
This is because indentation in Markdown has a special meaning (in
particular, to indicate that text should be formatted with a
fixed-width font). The indentation of % for
, % endfor
, % if
,
and % endif
is not necessary, but it helps make the code more readable.
Note that the line ${ c } is allergic to ${ c.allergy }
makes use of
the fact that the textual representation of a DAList
is the result
of running the comma_and_list()
method on the list. So the
resulting sentence might be “Jane Doe is allergic to shellfish,
peanuts, and dust.”
Mixed object types
If you want to gather a list of objects that are not all the same
object type, you can do so by setting the .ask_object_type
attribute
of the list to True
providing a block that defines the
.new_object_type
attribute of the list.
In this example, we have a list called location
, which is a type of
DAList
. We have a mandatory
code
block that
sets location.ask_object_type
to True
. This instructs
docassemble that location
is a list of objects, and that when a
new item is added to the list, docassemble should to look for the
value of location.new_object_type
to figure out what type of object
the new item should be. By contrast, the .object_type
attribute
instructs docassemble that the object type for every new object
should be the value of .object_type
.
Thus, before docassemble adds a new item to the list, it will seek
a definition of location.new_object_type
and then the item it adds
to the list will be an object of the type indicated by the value of
location.new_object_type
. After each item is added, docassemble
forgets about the value of location.new_object_type
, so the
question will be asked again for each item in the list.
There are a few things to note about the question
that defines
location.new_object_type
.
This a question about an item in a list, but note that we do not have
a variable i
to indicate which item it is, since .new_object_type
is an attribute of the list location
, not an attribute of the new
object (location[i]
). Thus, we have to use the
.current_index()
method to obtain the number.
Note also that we are using the method of
embedding a code block within a multiple choice question in order to
set the value of location.new_object_type
based on user input. You
might think it would be simpler to just write the following:
However, this would set location.new_object_type
to a piece of text
('Address'
or 'City'
), instead of the object type (Address
or
City
). Thus, when setting .new_object_type
(or .object_type
),
make sure to use Python code.
If you don’t want to use a buttons
interface, you can use code such
as the following to set the .new_object_type
attribute to a Python
class.
Running del location.new_object_type_selection
causes
location.new_object_type_selection
to be undefined. This ensures that
the next time location.new_object_type
is sought, the question
will
be asked again.
Note that there are two questions that ask about attributes of the list items:
You might be wondering how docassemble knows which of these two
questions to ask for a given item in the location
list. If the
object is a City
, a textual representation of the object will first
ask for .city
and then .state
. If the object is an Address
, a
textual representation of the object will first ask for .address
.
When docassemble gathers items into a list, it asks whatever
questions are necessary to construct a textual representation of the
item. So if the attribute docassemble needs is .city
, both
questions are capable of defining that attribute. The “What is the
city” question comes last in the YAML file, so it takes precedence
over the “What is the address” question, and it will be asked. If the
attribute docassemble needs is .address
, only the “What is the
address” question is capable of defining that, so only that question
will be asked.
If you set .ask_object_type
to True
and you want docassemble
to query for the .new_object_type
, you need to trigger the list
gathering process by directly or indirectly calling .gather()
on the
list. If you try to bypass the list gathering process, you may
encounter problems. For example, this will result in an error:
Instead, make sure the interview logic triggers the list gathering process. For example:
Gathering information into dictionaries
The process of gathering the items in a DADict
dictionary is
slightly different from the process of gathering the items of a
DAList
. Like the gathering process for DAList
objects, the
gathering process for DADict
objects will call upon the attributes
.there_are_any
and .there_is_another
.
In addition, the process will look for the attribute .new_item_name
to get the key to be added to the dictionary. In the example below,
we build a DADict
in which the keys are the names of fruits and
the values are the number of seeds that fruit contains. There is one
question that asks for the fruit name (fruit.new_item_name
) and a
separate question that asks for the number of seeds (fruit[i]
).
(When populating a DADict
, i
refers to the key, whereas when
populating a DAList
, i
refers to a number like 0, 1, 2, etc.)
Alternatively, you can use the attribute .new_item_value
to set the
value of a new item.
The value of the .new_item_value
attribute will never be sought by
the gathering process; only the value of the
.new_item_name
attribute will be sought. So if you want to use
.new_item_value
, you need to set it using a question that
simultaneously sets .new_item_name
, as in the example above.
Gathering a dictionary of objects
You can also populate the contents of a DADict
in which each value
is itself an object.
In the example above, we populate a DADict
called pet
, in which
the keys are a type of pet (e.g., 'cat'
, 'dog'
), and the values
are objects of type DAObject
with attributes .name
(e.g.,
'Mittens'
, 'Spot'
) and .feet
(e.g., 4
). We need to start by
telling docassemble that the DADict
is a dictionary of
objects. We do this by setting the .object_type
attribute of the
DADict
to DAObject
. Then we provide a question that sets the
.new_item_name
attribute.
When a .object_type
is provided, docassemble will take care of
initializing the value of each entry as an object of this type. It
will also automatically gather whatever attributes, if any, are
necessary to represent the object as text. The representation of the
object as text is what you see if you include the object in a Mako template:
${ pet['cat'] }
. (Or, if you know Python, it is the result of
str(pet['cat'])
.) The attributes necessary to represent the object
as text depend on the type of object. In the case of a DAObject
,
no attributes are required to represent the object as text. In the
case of an Individual
, the individual’s name is required
(.name.first
at a minimum).
Since a DAObject
does not have any necessary attributes, then in
the example above, the pet
object is considered “gathered”
(i.e. pet.gathered
is True
) after all the types of pet (e.g.,
'cat'
, 'dog'
) have been provided. At this point, the values of
the DADict
are simply empty DAObject
s. The .name
and
.feet
attributes are still not defined. The final screen of the
interview, which contains a “for” loop that describes the number of
feet of each pet, causes the asking of questions to obtain the .feet
and .name
attributes.
Gathering information into sets
The gathering of items into a DASet
is much like the gathering of
items into a DADict
. The difference is that instead of using the
attributes .new_item_name
and .new_item_value
, you use a single
attribute, .new_item
.
Here is an example that gathers a set of text items (e.g., 'apple'
,
'orange'
, 'banana'
) into a DASet
.
You can also gather objects into a DASet
. However, the DASet
does not use the .object_type
attribute, as DAList
and
DADict
groups do. The objects that you gather into a DASet
need to exist already.
In the example below, we create several DAObject
s, each
representing a fruit, and we use a multiple choice question with
datatype
set to object
to ask which fruits the user likes. (See
selecting objects for more information about these types of
questions.)
Manually triggering the gathering process
In the examples above, the process of asking questions that populate
the list is triggered implicitly by code like ${ fruit.number() }
,
${ fruit }
or % for item in fruit:
.
If you want to ask the questions at a particular time, you can do so
by referring to fruit.gather()
. (Behind the scenes, this is the
same method used when the process is implicitly triggered.)
The .gather()
method accepts some optional keyword arguments:
minimum
can be set to the minimum number of items you want to gather. The.there_are_any
attribute will not be sought. The.there_is_another
attribute will be sought after this minimum number is reached.number
can be set to the total number of items you want to gather. The.there_is_another
attribute will not be sought.item_object_type
can be set to the type of object each element of the group should be. (This is not available forDASet
objects.)complete_attribute
can be set to the name of an attribute that should be sought for each item during the gathering process. You can also set thecomplete_attribute
attribute of the group object itself.
The .gather()
method is not the only way that a gathering process
can be triggered. The .auto_gather
attribute controls whether the
.gather()
method is invoked. If .auto_gather
is True
(which is
the default), then the gathering process will be triggered using
.gather()
. If .auto_gather
is False
, the gathering process will
be triggered in a simpler way: by seeking the value of .gathered
.
Thus, you can provide a code
block that sets .gathered
to
True
. For example:
Setting .gathered
to True
means that when you try to get the
length of the group or iterate through it, docassemble will assume
that nothing more needs to be done to populate the items in the group.
You can still add more items to the list if you want to, using
code
blocks.
Detailed explanation of gathering process
At a very basic level, it is not complicated to gather a list of things from a user. For example, you can do this:
This example uses Python’s built-in range()
function, which
returns a list of integers starting with the first argument and less
than the second argument. For example:
The for
loop iterates through all the numbers using the variable
index
, looking for fruit[index]
. The first item it looks for is
fruit[0]
. Since this is not defined yet, the interview looks for a
question that offers to define fruit[0]
. It does not find any
questions that define fruit[0]
, so it then looks for a question that
offers to defined fruit[i]
. It finds this question, and asks it of
the user. After the user provides an answer, the for
loop runs
again. This time, fruit[0]
is already defined. But on the next
iteration of the for
loop, the interview looks for fruit[1]
and
finds it is not defined. So the interview repeats the process with
fruit[1]
. When all of the fruit[index]
are defined, the
mandatory
question is able to be shown to the user, and the
interview ends.
Another way to ask questions is to ask for one item at a time, and after each item, ask if any additional items exist.
To gather the list manually, it is necessary to disable the automatic gathering system:
This example uses a little bit of Python code to ask the appropriate questions.
Some variables are initialized:
Then the main algorithm is:
Since more_fruits
is initialized as 0
, the first undefined
variable that this code encounters is fruit[0]
. When the code
encounters fruit[0]
, it will go looking for the value of fruit[0]
,
and the question “What’s the first fruit?” will be asked. Once
fruit[0]
is defined, the interview undefines more_fruits
, but then
when the while
loop loops around, the definition of more_fruits
is
needed. Since more_fruits
is undefined, the interview presents the
user with the more_fruit
question, which asks “Are there more
fruits?” If more_fruits
is True
, the loop repeats, and the
definition of fruit[1]
is sought.
This is starting to get complicated. And things get even more complicated when you want to say things like “There are three fruits in all” and “You have told me about three fruits so far” in your interview questions. In the case of “There are three fruits in all,” a prerequisite to saying this is to make sure that the user has supplied the full list. But in the case of “You have told me about three fruits so far,” you would not want this prerequisite.
Since asking users for lists of things can get complicated, docassemble has a feature for automating the process of asking the necessary questions to fully populate the list.
If your list is fruit
, there are three special attributes:
fruit.gathered
, fruit.there_are_any
, and fruit.there_is_another
.
The fruit.gathered
attribute is initially undefined, but is set to
True
when the list is completely populated. The
fruit.there_are_any
attribute is used to ask the user whether the
list is empty. The fruit.there_is_another
attribute is used to ask
the user questions like “You have told me about three fruits so far:
apples, peaches, and pears. Are there any additional fruits?”
In addition to these two attributes, there is special method,
fruit.gather()
, which will cause appropriate questions to be asked
and will return True
when the list has been fully populated. The
.gather()
method looks for definitions for fruit.there_are any
,
fruit[i]
, and fruit.there_is_another
. It makes
fruit.there_is_another
undefined as necessary.
Here is a complete example:
Avoiding triggering the gathering process
docassemble implicitly calls .gather()
in many circumstances,
such as when you do for item in my_list:
, len(my_list)
, or
my_dict.items()
. In some situations, you may want to use a DAList
,
DADict
, or DASet
while the gathering process is still going on, or
has not been started yet.
To test whether a group has been gathered, you can call
.has_been_gathered()
on it. This will return True
if the group
has been gathered, and False
otherwise.
To test whether the gathering process has been started, you can access
the .gathering_started
attribute.
To get the number of items in a group without triggering the gathering
process, call .number_gathered()
. This will return the number of
items gathered so far.
To sort a group even if it has not been fully gathered yet, call
.sort_elements()
instead of .sort()
.
The DAList
, DADict
, and DASet
objects have an attribute
.elements
that is a plain Python list
, dict
, or set
containing
the items in the group. To bypass the special features of DAList
,
DADict
, and DASet
, you can access .elements
directly, and the
list gathering process will not be triggered.
When the gathering process is still going on and your group contains
objects, .elements
may contain one or more items that are not
usable. For example, when the interview is in the process of asking
for the fifth item in the group, you may want to show the user the
first four items. However, if you try to loop over .elements
and
display information about each one, you may find yourself in a
Catch-22 because your code expects attributes of the fifth item to be
defined when those attributes are defined by the very same
question
you are trying to ask. Instead of accessing .elements
,
you can call .complete_elements()
. This will return a DAList
,
DADict
, or DASet
containing only elements that are “complete.”
Whether an item is “complete” depends on whether the group has a
complete_attribute
. If the group has a complete_attribute
, an
item in the group will be considered “complete” if the item has an
attribute by the name of the complete_attribute
. If the group does
not have a complete_attribute
, an item will be considered
“complete” if it can be reduced to text without encountering an
undefined variable. For example, an Individual
object can be
reduced to text if the .name.first
attribute is defined, so if a
DAList
called my_list
contains several Individual
objects,
my_list.complete_elements()
will return a DAList
containing a
only those objects where .name.first
is defined.
Using “for loops”
In computer programming, a “for loop” allows you to do something repeatedly, such as iterating through each item in a list.
For example, here is an example in Python:
This code “loops” through the elements of numbers
and computes the
total amount. At the end, 14
is printed.
For loops based on DAList
, DADict
, and DASet
objects can
be included in textual content using the for
/endfor
Mako statement:
Mako “for” loops work just like Python for loops, except that they need to be ended with “endfor.”
If the list might be empty, you can check its length using an
if
/else
/endif
Mako statement:
You can also use the .number()
method:
You can check if something is in a list using a statement of the form
if
… in
:
For more information about “for loops” in Mako, see the markup section.
Edit an already-gathered list
It is possible to allow your users to edit a DAList
list that has
already been gathered. Here is an example.
This works using two features:
- The
edit
specifier on thetable
block, which adds an “Actions” column to the table and indicates which screens should be shown when the user clicks the “Edit” button. First a screen will be shown that asks for the the attribute.name.first
. Then a screen will be shown that asks for the attribute.favorite_fruit
. - The
.add_action()
method on theDAList
inserts HTML for a button that the user can press in order to add an entry to an already-gathered list.
You can allow your users to edit a DAList
from an edit button in a
review
page.
The attribute .revisit
of a DAList
is special; it is undefined
by default and is set to True
by the auto-gathering process at the
same time that .gathered
is set to True
. Because .revisit
is
undefined at first, the review
block will not show the “Edit”
button for the list until the list is gathered. When the list has
been gathered, and the user clicks the “Edit” button associated with
.revisit
, the user is taken to the block with continue button field:
person.revisit
. On this screen, you can show the list as a table
and provide the .add_action()
button if you want users to be able to
add entries.
Putting an editable table directly into a review page is also possible.
The line need: person.table
is important here. An item in a
review
list will not be shown if it contains any undefined
variables. The presence of an undefined variable in a review
list
item will not cause docassemble to seek a definition of that
variable (unless the specifier skip undefined: False
is used).
Therefore, if you want a review
item containing a table
to be
displayed, you need to make sure that the variable representing the
table
gets defined by the time that you want the table to be
editable. In this example, need: person.table
ensures that the
variable representing the table is defined before the user is given
the opportunity to review his or her answers.
While the above examples have all featured tables for editing DAList
objects, the edit
feature can also be used when the rows
of the
table
refer to a DADict
:
If your DAList
is not made up of objects, it can be made editable
by setting edit
to True
instead of to a list of attributes.
You can do the same with DADict
groups that do not use objects:
Customizing the editing interface
If you do not want your users to be able to delete items, you can add
delete buttons: False
to the table
.
Or, if you want your users to be able to delete items, but not edit
items, you can include delete buttons: True
and do not include
edit
:
If you want to allow your users to delete items, but only if the group
is longer than a certain length, you can give the DAList
or
DADict
a minimum_number
attribute.
If you have a DAList
or a DADict
and you
want the user to confirm before deleting an item that they really
meant to delete the item, you can include confirm: True
.
If you want specific items to be protected
against editing and/or deletion, you can set a read only
specifier:
In this example, the attribute important
of the table fruit
determines whether the item is “read only” or not. The first two
items in the DAList
, which are added to the list in a code
block,
have the important
attribute set to True
, while items that are
added by the user have the important
attribute set to False
.
Since read only
is set to important
, the Edit
and Delete
buttons are not available for the items that have the important
attribute set to True
.
If you want to allow editing but not deletion, or vice versa, the
value of the attribute can be set to a Python dictionary rather than
the value True
or False
. If the value of the key edit
is false,
the “Edit” button will not be shown. If the value of the key
delete
is false, the “Delete” button will not be shown.
Typically, the creation of a table
requires the gathering process to be completed. However, if the
gathering process is already ongoing, then the table will still be
created, and it will only contain items that are “complete.” If you
do not want the showing of the table
to trigger the gathering
process, set require gathered: False
.
If you have a table
definition that includes
editable elements (i.e. edit
, delete buttons
), but you want to
present the table with the editing features in some contexts, but
without the editing features in other contexts, you can include the
table by calling the method .show()
with editable=False
to hide
the editing features.
Canceling an add or edit process
If you want to allow the user to cancel the process of adding or
editing the item of a DAList
, you can offer the user an “action”
that runs the .cancel_add_or_edit()
method on the list.
This will cancel any pending actions related to the list and delete the last item in the list if it is incomplete.
In this example, using show if: fruits.has_been_gathered()
was not
required; you may want to use that so that the “Cancel” buttons are
not shown until the list is edited after having been gathered.
Reorder an already-gathered list
If you have a DAList
and you want to allow the user to change the
order of items in the list, you can set allow reordering
to True
:
The changes to the order of elements will be saved when the user presses Continue.
Use a table to gather the list
It is possible to use a table
for the initial gathering process, not
just for editing an already-gathered list. You can do this by
initializing the list with gathered=True
and then placing something
in the interview logic that forces the user to visit a page with a
table
and an .add_action()
button.
In this example, it is important that parties.reviewed
is placed
into the interview logic in the place where the user should be asked
for the names of the parties. If this variable is not required, the
parties
list will simply be considered to be empty.
Collect all items on one page
By default, when gathering or editing a list item, docassemble
asks about only one list item at a time. If you have a question
that contains a fields
specifier and that uses iterator variables
(i
, j
, k
etc.) in the variable names, you can use list collect
to expand this question
on the screen so that the user can enter
answers for multiple list items on one screen.
The list collect
specifier can be set to True
, False
, or
Python code that evaluates to a true or false value. If the value
is true, the question
will be expanded; if it is false, the
question
will not be expanded.
A limitation of the list collect
feature is that you cannot use
Mako templating on field labels, or else an error will result.
You can customize the behavior of the question
by setting list
collect
to a dictionary.
The available keys for the dictionary are:
enable
: this can beTrue
,False
, or Python code that evaluates to a true or false value. If the value is true, thequestion
will be expanded; if it is false, thequestion
will not be expanded. (This is the same as the value for the shorthand version oflist collect
discussed above.) Iflist collect
is a dictionary andenable
is omitted, the default value isTrue
.label
: this can be set to a Mako text label for each item on the screen. If it isFruit ${ i + 1 },
the items will be labeled “Fruit 1,” “Fruit 2,” etc.is final
: this can beTrue
,False
, or Python code that evaluates to a true or false value. If the value is true, then thethere_is_another
attribute will be set toTrue
when the user presses the Continue button. The default value isTrue
.allow append
: this can beTrue
,False
, or Python code that evaluates to a true or false value. If the value is true, then the user is allowed to add additional items to the list. If the value is false, the user can only edit the existing items. The default value isTrue
.allow delete
: this can beTrue
,False
, or Python code that evaluates to a true or false value. If the value is true, then the user is allowed to delete existing items from the list. If it is false, the user will not see any “Delete” buttons except on items that appear because the user clicked the “Add another” button.add another label
: you can set this to text that will used of “Add another” for the button that adds another item to the list. The default text can be globally changed using thewords
feature.
Here is an example:
This example demonstrates how you can use the enable
attribute to
indicate that the multiple-item collection method should be used to
gather the list initially, but that the ordinary one-item-per-screen
method should be used for editing list elements or adding new list
elements after the list is initially gathered.
If you set a the minimum_number
attribute on the DAList
to 3,
the first three items in the list will not have Delete buttons.
The list collect
specifier only works on DAList
variables, not
on DADict
or DASet
variables.
Triggering your own code during the gathering process
When you are gathering a group, you might want some code to run after the group is gathered, as well as whenever an item in the group is edited or deleted.
If the code you need to run relates only to an item in the group, you
can set complete_attribute
to 'complete'
and write a code
block that defines the .complete
attribute for any item in the
group. This code
block will be run for every item in the group
during the gathering process and also whenever the user edits the
group.
However, sometimes the code you want to run relates to the group as a
whole and not just to a particular item. In this circumstance, you
can use the “hook” methods hook_on_gather()
and
hook_after_gather()
. In order to use these methods, you will need
to define your own class using a module file, and make your class a
subclass of whatever class you are using (e.g., DAList
,
DADict
).
Here is an example that subclasses the DADict
.
The module file that is referenced in the modules
block,
income.py
, looks like this:
In this example, the hook_on_gather()
method ensures that the user
provides an explanation about their income if the user is employed,
receives public benefits, and the total income from these income
sources exceeds $2,000.
The hook_after_gather()
method performs a computation that uses
all of the items in the dictionary.
The advantage of putting this logic into the “hook” methods is that
the logic will be applied automatically whenever a change is made to
the items in the dictionary. For example, if the user first puts in
less than $2,000 of income but then edits the list to increase the
income, the additional question will be asked. If the user edits the
list to decrease the income below $2,000, the attribute with the
answer to that question is deleted. Whenever a change is made to the
list, the total_amount
is updated.
The hook_on_gather()
method is run just before the dictionary is
marked as gathered. Every time the user edits the table, the
dictionary is temporarly marked as ungathered, and is then marked as
gathered again. Since the dictionary can’t be marked as gathered
until the hook_on_gather()
method runs to completion, you can be
sure that the .reason_for_benefits
attribute will get defined (or
undefined if appropriate).
By contrast, the hook_after_gather()
method is run after the
dictionary is marked as gathered. It is guaranteed to run after the
group is gathered or re-gathered. Unlike hook_on_gather()
, it
cannot trigger the asking of question
blocks or code
blocks,
at least not in a reliable way.
In this example, the hook_after_gather()
method computes a sum.
This is done for demonstration purposes only. In practice, if you
just need to compute a sum, it is best to write a separate method for
this, and then call that method whenever you need the sum. (You can
also write code in-line that computes the sum.) You might want to use
hook_after_gather()
for code that calls an API, or other code that
should not run unnecessarily.
Note that since hook_on_gather()
is called during the gathering
process, it is careful not to do anything that relies upon the group
being gathered. For example, it refers to the .elements
dictionary
directly, which will not trigger gathering. By contrast, the
hook_after_gather()
method assumes (correctly) that the group has
already been gathered.
Subclassing the DADict
is an advanced Python technique, but it
is ultimately easiest to write your logic in the form of “hooks,”
because otherwise you have to try to anticipate all the different ways
that users might be able to get past your logic by editing, deleting,
or adding table items.
Examples
List of dictionaries from checkbox
Here is an example of an interview that uses a checkbox to determine which items to use in a dictionary.
Prepopulate a list
Here is an example of an interview that populates a list with two entries before allowing the user to add other entries.
This interview takes advantage of the fact that the automatic
gathering process will seek a definition of the .there_are_any
attribute. It uses the code block that defines .there_are_any
to
populate the list of objects.
Note that user.favorite_things.clear()
is called. This line happens
to be unnecessary in this interview, but it illustrates a good
practice. Code blocks in docassemble often need to be
idempotent; they should be able to be run from the beginning more
than once without causing unwanted side effects. Code blocks often
restart because when an undefined variable is encountered and the
definition is retrieved from the user or from another code block, the
original code block does not pick up where it left off, but rather
starts at the beginning again.
Alternatively, you could prepopulate a list by using mandatory
code at the beginning of an interview to append items to the list.
Then the interview will never even seek a definition of the
.there_are_any
attribute. The method described above is helpful,
however, in cases where the list being initialized does not exist at
the start of the interview, as would be the case if the list was
user.sibling[i].favorite_things
.
Postpopulate a list
Here is an example of an interview that populates a list with two entries after the user is done adding entries.
This interview uses code blocks to determine
user.favorite_things.there_are_any
and
user.favorite_things.there_is_another
. Instead of asking the user
questions that define these variables directly, the interview asks
questions that set the variables user.likes_something
and
user.likes_another_thing
. It can then use code to do things
depending on what the answers are.
If the user says he has no favorite things, the interview adds Mom and
apple pie to user.favorite_things
. If the user does describe some
favorite things, and then says that he has no other favorite things,
the interview will then add Mom and apple pie to the list.
Note that if the user says he has no favorite things, the interview
sets .there_is_another
to False
. This is necessary to persuade the
automatic gathering feature that the list is fully gathered.
Note the use of del
to undefine user.likes_another_thing
as soon
as it is set to True
. This is because the automatic gathering
system will need to ask the question again, and if
user.likes_another_thing
is already set to True
, the list of the
user’s favorite things will be infinite! Similarly, behind the
scenes, the automatic gathering process undefines .there_is_another
after it is defined.