Edit this page on GitHub

Objects

How docassemble uses objects

Python allows object-oriented programming and so does docassemble.

Object-oriented programming can seem complicated at first, but it actually makes programming much easier. For an easy-to-read introduction to object-oriented programming, see Object-oriented Programming for Document Assembly Developers by Quinten Steenhuis.

Here is a non-object-oriented way of saying hello to the user by name:

question: What is your name?
fields:
  - First: user_first_name
  - Last: user_last_name
---
question: |
  Hello, ${ user_first_name } ${ user_last_name }!
mandatory: True
hello-not-oop

A better way is to define user as a docassemble object, Individual.

modules:
  - docassemble.base.util
---
objects:
  - user: Individual
---
question: |
  What's your name?
fields:
  - First: user.name.first
  - Last: user.name.last
---
question: |
  Hello, ${ user }!
mandatory: True
hello-oop

As explained in the fields section, variable names cannot contain any punctuation other than the underscore. So while user_first_name is a valid variable name, user.name.first must be referring to something different. Periods in Python are used to refer to the “attributes” of “objects.”

An object is a special type of variable. Rather than being a piece of text, like user_first_name is, the variable user is an “object” that is an “instance” of the “class” known as Individual.

Using objects in docassemble requires a little bit of setup using initial blocks. Individual is defined in the docassemble.base.util Python module, so it was necessary to bring that module into the interview with a modules block. It was also necessary to use an objects block to declare that user is an instance of the class Individual.

Objects have “attributes.” In the above example, name is an attribute of the object user. And name is itself an object (it is an instance of the class IndividualName, though you would need to look at the source code to know that) with attributes first and last. The attributes first and last are not objects, but rather pieces of text. Anything before a . is an object, and anything after the . is an attribute of the object.

Objects also have “methods,” which are functions that return a value based on the attributes of the object. For example, user.age_in_years() will return the current age of the user based on the date defined in the attribute user.birthdate:

question: |
  What is your date of birth?
fields:
  - no label: user.birthdate
    datatype: date
---
question: |
  You are ${ user.age_in_years() }
  years old.
mandatory: True
age_in_years

Methods are similar to attributes in that they are written with a . before them. The difference is that they run code to produce a value, rather than simply accessing a stored value. You can tell by the presence of parentheses whether a method is being used.

Using objects in your interviews has a number of advantages over using plain variables.

The first advantage is that you can write generic object questions. (See modifiers for documentation of the generic object feature.)

For example, if you need to collect the phone numbers of three people, the grantor, the grantee, and the trustee, you don’t have to write separate questions for grantor.phone_number, grantee.phone_number, and trustee.phone_number. You can write one question to collect x.phone_number, where x is a “generic object” that acts as a stand-in for any object of type Individual.

objects:
  - grantor: Individual
  - grantee: Individual
  - trustee: Individual
---
generic object: Individual
question: |
  What is
  ${ x.object_possessive('name') }?
fields:
  - First Name: x.name.first
    default: ${ x.first_name_hint() }
  - Middle Name: x.name.middle
    required: False
  - Last Name: x.name.last
    default: ${ x.last_name_hint() }
  - Suffix: x.name.suffix
    required: False
    code: |
      name_suffix()
generic-object-phone-number

Any time docassemble needs to know the phone number of an Individual, this question will allow it to ask the appropriate question.

In the question text above, possessive() is a “method” that you can use on any instance of the Individual class. If trustee’s name is Fred Smith, trustee.possessive('phone number') returns “Fred Smith’s phone number.” The method is pretty smart; user.possessive('phone number') will return “your phone number.”

Using objects also allows you to have different variables that refer to the exact same thing. For example, if user is already defined as an object and you run this code:

trustee = user

then you will define the variable trustee as being equivalent to the user object. trustee.name.first will always return the same thing as user.name.first, and trustee.phone_number will always return the same thing as user.phone_number. In addition, trustee.possessive('phone number') will return “your phone number.” You can write code that checks for the equivalence of objects, using the is operator:

mandatory: True
question: |
  % if user is trustee:
  As the trustee of the estate, you need to understand that it is
  your fiduciary duty to safeguard the assets of the estate.
  % elif user is grantee:
  You are the grantee, which means that ${ trustee } is required to
  safeguard the assets of the estate on your behalf.
  % else:
  ${ trustee } will safeguard the assets of the estate on behalf of
  ${ grantee }.
  % endif
user-is-trustee

Object methods allow you to have a standard way of expressing information even though the methods used to gather the information may vary depending on the circumstances. For example, the age_in_years() method, discussed above, first looks to see if the attribute age is defined, and if so will return that instead of asking for the birthdate attribute:

objects:
  - user: Individual
---
question: |
  How old are you?
fields:
  - Age in years: user.age
    datatype: number
---
mandatory: True
code: |
  need(user.age)
---
mandatory: True
question: |
  You are ${ user.age_in_years() } years old.
testage2

Although objects are a fairly complicated concept, as you can see, they allow you to write code that looks much like plain English.

In part, this is because objects allow you to do complicated things in an implicit way. For example, writing ${ grantee } in a Mako template will return the name of the grantee. The interview implicitly calls the method __str()__ on grantee. grantee.__str()__ in turn calls grantee.name.full(), which strings together the grantee’s full name from its constituent parts (name.first, name.middle, name.last, and name.suffix), all but the first of which are optional and will not be included if they are not defined.

Note that object methods may depend upon particular attributes of objects being defined. If an attribute is needed but not defined, docassemble will go looking for a question or code block that defines the attribute. For example, if you write this in a question:

Remember that ${ trustee.possessive('phone number') } is
${ trustee.phone_number }.

then in order to ask the question, docassemble may ask you for the trustee’s name (so it can say “Remember that John Smith’s phone number is …”), and then ask for the trustee’s phone_number if it is not already defined.

Standard docassemble classes

To use the classes described in this section in your interviews, you need to include them from the docassemble.base.util module by writing the following somewhere in your interview:

modules:
  - docassemble.base.util

Unless otherwise instructed, you can assume that all of the classes discussed in this section are available in interviews when you include this modules block.

DAObject

All docassemble objects are instances of the DAObject class. DAObjects are different from normal Python objects because they have special features that allow their attributes to be set by docassemble questions. If fruit is an ordinary Python object and you refer to fruit.seeds when seeds is not an existing attribute of fruit, Python will generate an AttributeError. But if fruit is a DAObject, docassemble will intercept that error and look for a question or code block that offers to define fruit.seeds. Or, if that does not work, it will look for a generic object block that offers to define x.seeds for a DAObject.

From the interview author’s perspective, DAObjects can be treated like ordinary Python objects in most ways, but there are exceptions.

An important characteristic of all DAObjects is that they have intrinsic names. If you do:

objects:
  - foo: DAObject

or you do:

code: |
  foo = DAObject()

then foo.instanceName will be 'foo'. The object knows its own name. This is not a standard feature of Python objects, but a feature added by docassemble.

Since foo is a Python object, you can create other names for the same object, but the instanceName attribute will not change.

>>> from docassemble.base.core import DAObject
>>> foo = DAObject()
>>> foo.instanceName
'foo'
>>> foo.seeds = 4
>>> foo.seeds
4
>>> bar = foo
>>> bar.instanceName
'foo'
>>> bar.seeds += 1
>>> foo.seeds
5

The fact that each DAObject has only one intrinsic name can lead to confusion in interviews if you are not careful. For example, suppose you try the following:

modules:
  - docassemble.base.core
---
objects:
  - tree: DAObject
  - long_branch: DAObject
---
mandatory: True
question: |
  The length of the branch is
  ${ tree.branch.length }.
---
code: |
  tree.branch = long_branch
---
question: |
  What is the length of the branch
  on the tree?
fields:
  - Length: tree.branch.length
branch-error

This will result in the following error:

There was a reference to a variable ‘long_branch.length’ that could not be looked up in the question file or in any of the files incorporated by reference into the question file.

You might think, “hey, why doesn’t my interview ask the question that sets tree.branch.length?” The reason is that tree.branch is just an alias for long_branch, and the object knows itself only as long_branch. Thus, when the interview needs a definition for the .length attribute of this object, it will look for long_branch.length.

If you had a question that defined long_branch.length or a generic object question for the x.length where x is a DAObject, then the interview would use that question. However, the interview is not able to search for the length of the branch using tree.branch.length since the intrinsic name of the object is long_branch, not tree.branch.

This will work as intended:

modules:
  - docassemble.base.core
---
objects:
  - tree: DAObject
  - tree.branch: DAObject
---
mandatory: True
question: |
  The length of the branch is
  ${ tree.branch.length }.
---
question: |
  What is the length of the branch
  on the tree?
fields:
  - Length: tree.branch.length
branch-no-error

One of the useful things about DAObjects is that you can write generic object questions that work in a wide variety of circumstances because the questions can use the variable name itself when forming the text of the question to ask the user.

If you refer to a DAObject in a Mako template (or reduce it to text with Python’s str() function), this will have the effect of calling the object_name() method, which attempts to return a human-friendly name for the object.

For example:

modules:
  - docassemble.base.core
---
objects:
  - park: DAObject
  - turnip: DAObject
---
mandatory: True
code: |
  park.front_gate = DAObject()
---
mandatory: True
question: |
  The ${ turnip.color } turnip sat
  before the
  ${ park.front_gate.color } gate.
---
generic object: DAObject
question: |
  What is the color of the ${ x }?
fields:
  - Color: x.color
daobject

Although there is only one question for x.color, this question generates both “What is the color of the turnip?” and “What is the color of the front gate in the park?” This is because object_name() is implicitly called and it turns park.front_gate into “front gate in the park.”

The object_name() method is multi-lingual-friendly. By using docassemble.base.util.update_word_collection(), you can provide non-English translations for words that come from variable names, such as “turnip,” “park,” and “front gate.” By using docassemble.base.util.update_language_function(), you can define a non-English version of the a_in_the_b() function, which object_name() uses to convert an attribute name like park.front_gate into “front gate in the park.” (It calls a_in_the_b('front gate', 'park').) So in a Spanish interview, park.front_gate.object_name() would return “puerta de entrada en el parque.” (The Spanish version of a_in_the_b() will be more complicated than the English version because it will need to determine the gender of the second argument.)

A related method of DAObject is object_possessive(). Calling turnip.object_possessive('leaves') will return the turnip's leaves. Calling park.front_gate.object_possessive('latch') will return the latch of the front gate in the park.

The DAObject is the most basic object, and all other docassemble objects inherit from it. These objects will have different methods and behaviors. For example, if friend is an Individual (from docassemble.base.util), calling ${ friend } in a Mako template will not return friend.object_name(); rather, it will return friend.full_name(), which may require asking the user for the friend’s name.

A DAObject can have any attributes you want to give it. When those attributes are objects themselves, you need to use the initializeAttribute() method.

One way to initialize attributes of an object is to use Python code:

objects:
  - fish: DAObject
---
code: |
  fish.best_friend = DAObject()

Under many circumstances, this works, and the variable on the left will be assigned a correct instanceName.

However, docassemble’s system for setting the instanceName in circumstances like this relies on hacking the internals of Python. It is not guaranteed to work in all circumstances. A safe way to define attributes is as follows:

objects:
  - fish: DAObject
---
sets: fish.best_friend
code: |
  fish.initializeAttribute('best_friend', DAObject)

The first argument to initializeAttribute is the attribute name, as quoted text. The second argument is the name of the object the attribute should be (not quoted).

It is necessary to modify the code block with sets because docassemble needs help figuring out that the code block offers to define fish.best_friend.

The initializeAttribute() method will have no effect if the attribute is already defined. If you want to force the setting of an attribute in situations when the attribute is already defined, use reInitializeAttribute() instead of initializeAttribute(), and it will overwrite the attribute.

Another way to define object attributes is to use the objects block.

objects:
  - fish: DAObject
  - fish.best_friend: DAObject

You can even use objects with the generic object modifier:

generic object: Person
objects: |
  - x.principal_place_of_business: City

This will ensure that the principal_place_of_business of an Individual or Organization is always a City.

The DAObject provides some convenience functions for working with object attributes.

The attribute_defined() method will return True or False depending on whether the given attribute is defined. The attribute name must be provided as quoted text. For example:

objects:
  - client: Individual
---
mandatory: True
question: |
  % if client.address.attribute_defined('city'):
  You live in ${ client.address.city }.
  % else:
  I don't know where you live.
  % endif

The attr() method will return the value of the given attribute. The attribute must be provided as text. (E.g., client.address.attr('city').) If the attribute is not defined, None will be returned. This can be useful if you have several attributes but you want to access them programmatically. For example:

mandatory: True
question: |
  Your address.
subquestion: |
  % for part in ['address', 'city', 'state', 'zip']:
  Your ${ part } is ${ client.address.attr(part) }.
  
  % endfor

Note that because None is returned when the attribute is not defined, this method will not trigger a process of retrieving a definition for the attribute. If you want to trigger this process, use the built-in Python function getattr().

code: |
  for characteristic in ['eye_color', 'hair_color', 'weight']:
    getattr(client.child[i], characteristic)
  client.child[i].complete = True

As discussed below, the Individual object has interesting methods related to pronouns. These methods are universal, so you can use them on any DAObject.

If the object is a generic DAObject, or a subclass of DAObject that does not have any special pronoun behavior, then the .pronoun(), .pronoun_subjective(), and .pronoun_objective() methods all return 'it'. The .pronoun_possessive() method returns 'its' followed by the argument. For example, thing.pronoun_possessive('reason') returns 'its reason'.

The .as_serializable() method returns a representation of the object and its attributes. Objects are converted to Python dicts, so that they can be serialized to JSON. The conversion is not reversible, and much information cannot be converted. Nevertheless, this can be a useful way to access information in your objects in other systems. See also the all_variables() function.

The copy_shallow() method creates a copy of the object and gives it a new intrinsic name.

new_object = old_object.copy_shallow('new_object')

The copy is “shallow,” which means that while new_object will be a new object with its own separate existence, sub-objects of the new object will simply be references to corresponding sub-objects of the original object.

>>> new_object is old_object
False
>>> new_object.sub_object is old_object.sub_object
True
objects:
  - orig_company: DAObject
---
code: |
  other_company = orig_company.copy_shallow('other_company')
copy-shallow

The copy_deep() method creates a copy of the object its sub-objects and gives it, and all of its sub-objects, new intrinsic names.

new_object = old_object.copy_deep('new_object')

Because the copy is “deep,” each sub-object has a separate existence:

>>> new_object is old_object
False
>>> new_object.sub_object is old_object.sub_object
False
objects:
  - orig_company: DAObject
---
code: |
  other_company = orig_company.copy_deep('other_company')
copy-deep

DAList

A DAList acts like an ordinary Python list, except that docassemble can ask questions to define items of the list.

Here is a simple “Mad Libs” interview that uses DALists to keep track of words:

objects:
  - adjective: DAList
  - noun: DAList
  - person: DAList
  - place: DAList
---
mandatory: True
question: A funny story
subquestion: |
  ${ person[0] } went to ${ place[0] }
  to buy
  ${ indefinite_article(noun[0]) }.

  At the ${ place[0] }, there was
  ${ indefinite_article(adjective[0])}
  ${ noun[1] }, which tried to zap
  ${ person[0] }.

  But luckily, ${ person[1] } came out
  of the back room just in time and
  lunged at the ${ noun[1] }, thereby
  saving the day.
buttons:
  - Do it again: restart
---
generic object: DAList
question: |
  % if i == 0:
  Give me
  ${ indefinite_article(x.object_name()) }.
  % else:
  Give me another ${ x.object_name() }.
  % endif
fields:
  - no label: x[i]
madlibs

The variable i is special. When the interview encounters person[0] and sees that it is undefined, it will go searching for a question that offers to define person[0]. If it does not find that, it will generalize and look for a question that offers to define person[i]. If that is not found, it will generalize further and look for a question that offers to define x[i]. Thus, the one generic object question, which defines x[i] where x is a DAList, will ask all of the questions in the interview.

For another example, suppose you want to work with a list of prospective recipients of an e-mail. You could define recipient as a DAList containing five Individuals

objects:
  - recipient: DAList
  - trustee: Individual
  - beneficiary: Individual
  - grantor: Individual
---
mandatory: True
code: |
  recipient.clear()
  recipient.append(trustee)
  recipient.append(beneficiary)
  recipient.append(grantor)
  recipient.appendObject(Individual)
  recipient.appendObject(Individual)
  recipient.gathered = True
---
mandatory: True
question: The recipients
subquestion: |
  % for person in recipient:
  ${ person } is a recipient.
  % endfor
---
generic object: Individual
question: |
  What is the name of the ${ x.object_name() }?
fields:
  - First Name: x.name.first
  - Last Name: x.name.last
---
generic object: Individual
question: |
  The ${ ordinal(i) } ${ x.object_name() } must have a name.  What is it?
fields:
  - First Name: x[i].name.first
  - Last Name: x[i].name.last
testdalist

This will result in the following five questions being asked:

  • What is the name of the trustee?
  • What is the name of the beneficiary?
  • What is the name of the grantor?
  • What is the name of the fourth recipient?
  • What is the name of the fifth recipient?

The DAList operates like a list in Python, but it also has some special methods. When adding a new item to the list, you should use the docassemble-specific method appendObject() method. This method is similar to the initializeAttribute() method we discussed earlier. Running recipient.appendObject(Individual) creates a new object of the class Individual and adds it to the list. In the example above, the first such object is the fourth item in the list, which means that the intrinsic name of the new object is recipient[3]. The result of using() can be used in place of the name of a class.used as the second parameter.

A DAList can be given a default object type, so that appendObject() can be called without an argument. This default object type is controlled by the .objectFunction attribute. For example, when a PartyList object is created, the .objectFunction attribute is set to Person.

If you want greater control over the way the questions are asked, you could add a generic object question that is specific to the recipients that were added with appendObject(). For example:

generic object: Individual
question: |
  The ${ ordinal(i) } ${ x.object_name() } must have a name.  What is it?
fields:
  - First Name: x[i].name.first
  - Last Name: x[i].name.last

The names of the fourth and fifth recipients are capable of being asked by this question, since the pattern x[i] (where x[i] is an Individual) matches the intrinsic names recipient[3] and recipient[4]. Since the other generic object question, which matches x (where x is an Individual) also matches recipient[3] and recipient[4], the order in which you list the questions in the YAML file will determine which one is chosen. Later-appearing questions take precedence, so you would need to place the second generic object question somewhere after the first generic object question in order for it to be chosen.

Other methods available on a DAList are:

  • append(item_to_append) - adds item_to_append to the end of the list. Just like the Python list method of the same name. It takes an optional keyword argument set_instance_name. If set_instance_name is True, then the object’s intrinsic name will be changed to that of a member of the list (e.g., my_list[2]). If set_instance_name is False (the default), then the object’s instance name will not be changed.
  • clear() - makes the list empty.
  • complete_elements() - returns the elements in the list that are “complete.” This is useful when you have a list of objects, and some objects are still in a nascent state, and you only want to use the objects that are “complete.” (See the discussion of .complete_attribute below.)
  • extend(extension_list) - adds the items in the extension_list to the end of the list. Just like the Python list method of the same name.
  • index() - given an item that exists in the list, returns the index number at which the item can be found.
  • pop() - removes an item from the list. Just like the Python list method of the same name.
  • first() - returns the first item of the list; error triggered if list is empty.
  • last() - returns the last item of the list; error triggered if list is empty.
  • item() - if fruit is a DAList, fruit.item(2) is equivalent to fruit[2], except that if fruit does not have an item 2, the result will be empty text. This is a helpful method in some contexts, such as fillable PDF forms. Empty text will also result if you try to use attributes on the result, so fruit.item(2).seeds or fruit.item(2).total_value() will also result in empty text.
question: |
  Describe the ${ ordinal(i) } fruit.
fields:
  - Name: fruit[i].name
  - Color: fruit[i].color
  - Seeds: fruit[i].seeds
    datatype: integer
---
question: |
  Are there any more fruit?
yesno: fruit.there_is_another
---
mandatory: True
question: |
  Description of ten fruits.
subquestion: |
  % for index in range(10):
  The name of the ${ ordinal(index) }
  fruit is ${ fruit.item(index).name }.
  It is ${ fruit.item(index).color }
  and it has
  ${ fruit.item(index).seeds } seeds.
  
  % endfor
item
  • does_verb(verb) - like the verb_present() function from docassemble.base.util, except that it uses the singular or plural form depending on whether the list has more than one item or not.
  • did_verb(verb) - like the verb_past() function from docassemble.base.util, except that it uses the singular or plural form depending on whether the list has more than one item or not.
  • as_singular_noun() - if the variable name is case.plaintiff, returns plaintiff; if the variable name is applicant, returns applicant.
  • as_noun() - if the variable name is case.plaintiff, returns plaintiffs or plaintiff depending on the number of items in the list; if the variable name is applicant, returns applicants or applicant depending on the number of items in the list. You can also give this function any arbitrary noun and it will pluralize it or not depending on whether the number of items in the list is more than one. E.g., client.child.as_noun('kid') will return 'kid' or 'kids'.
  • number() - returns the total number of items in the list. If necessary it will trigger questions that ask for all of the items of the list to be populated.
  • number_as_word() - same as number(), except that the nice_number() function is applied to the result.
  • current_index() - similar to number(), except it returns the index value of the last item in the list. If there is one item in the list, current_index() returns 0. If there are two items, it returns 1. If there are no items in the list, it returns 0. This is useful in questions where an iterator i is not available, such as question that set .new_object_type.
  • remove() - removes the given items from the list, if they are in the list.
  • comma_and_list() - returns the items of the list run through the comma_and_list() function.
  • possessive() - if the variable name is plaintiff and the target is "fish", returns “plaintiff’s fish” if there is one item in the list and “plaintiffs’ fish” if there is more than one item in the list.
  • pronoun() - returns a pronoun like “you,” “her,” or “him,” “it”, or “them,” as appropriate, depending on the number of items in the list.
  • pronoun_objective() - for a DAList, this is the same as pronoun().
  • pronoun_possessive() - given a word like “fish,” returns “her fish,” “his fish,” or “their fish,” as appropriate, depending on the number of items in the list.
  • pronoun_subjective() - returns a pronoun like “you,” “she,” “he,” or “they” as appropriate, depending on the number of items in the list.
  • union(other_set) - returns a Python set consisting of the items of current list, considered as a set, combined with the items of the other_set.
  • intersection(other_set) - returns a Python set consisting of the items of the current list, considered as a set, that also exist in the other_set.
  • difference(other_set) - returns a Python set consisting of the items of the current list, considered as a set, that do not exist in the other_set.
  • isdisjoint(other_set) - returns True if no items overlap between the current list, considered as a set, and the other_set. Otherwise, returns False.
  • issubset(other_set) - returns True if the current list, considered as a set, is a subset of the other_set. Otherwise, returns False.
  • issuperset(other_set) - returns True if the other_set is a subset of the current list, considered as a set. Otherwise, returns False.
  • gather() - causes the items of the list to be gathered and named. Returns True.
  • reset_gathered() - causes the list to be considered not-yet-gathered. This can be used to query the user to add items to a list that may already be considered complete. If called with the optional keyword argument recursive set to True (the default is False), the lists inside the list are also marked as not-yet-gathered.
  • has_been_gathered() - returns True if the group has been gathered yet. Does not trigger the gathering process.
  • add_action() - returns HTML for a button that adds a new item to the list. This will set up a queue of actions for the user that will ensure that a new item will be added and its elements gathered. See the groups section for details of how DAList and DADict gathering works. By default, the text of the button is “Add an item” if the list is empty, and “Add another” if the list is non-empty. The message can be overridden with the optional keyword parameter message. The message passes through the word() function, so you can use the translation system to handle different languages. If you just want the URL for the action, not the HTML for the button, set the optional keyword parameter url_only to True.
  • item_actions() - returns HTML for “Edit” and “Delete” buttons. This method is primarily used internally; there are directives of the table that control it. It takes two positional parameters: the item itself (the_group[the_index]) and its index (the_index). It also accepts optional keyword parameters. If edit is false, the edit button is not shown. If delete is false, the delete button is not shown. If edit_url_only is true, a plain URL for the edit action is returned, rather than HTML. If delete_url_only is true, a plain URL for the delete action is returned, rather than HTML.

If you refer to a list in a Mako template (e.g., The applicants include: ${ applicant }) or convert it to text with the str() function (e.g. (str(applicant)) in Python code, the result will be the output of the comma_and_list() method.

The DAList uses the following attributes:

  • object_type: a class of type DAObject or subclass thereof, or None. Initially, this is set to None. If set to an object type, such as DAObject or Individual, then new items will be created as objects of this type. You can also use the result of the using() method here.
  • gathered: a boolean value, initially undefined. It is set to True when all of the items of the list are defined.
  • elements: a Python list containing the items of the list. If this is set, the list will be considered gathered as soon as it is initialized.
  • set_instance_name: this can be used in combination with elements. If set to True, then when the elements are added to the list, their instrinsic names (see above) are changed to match the intrinsic name of the list. For example, if you initialize parties as a DAList using elements=[plaintiff, defendant] and set_instance_name=True, then the name of plaintiff will be changed to parties[0] and the name of defendant will be changed to parties[0]. The variables plaintiff and defendant will still exist, but if your interview refers to an undefined attribute plaintiff.net_worth, the interview will seek a definition of parties[0].net_worth.
  • are_there_any: a boolean value, initially undefined, indicating whether any values should be gathered. The expectation is that the interview will define a question or code block that defines this attribute.
  • is_there_another: a boolean value, initially undefined, indicating whether there are any additional values that should be gathered.
  • auto_gather: a boolean value, set to True by default, which indicates whether the interview should use the .gather() method to ask questions to gather the items of the list.
  • complete_attribute: a text string indicating the name of an attribute of each item of the list. If you have a DAList called fruit and you set fruit.complete_attribute = 'weight', then when the .gather() method is gathering the items of the list, it will seek a definition of fruit[i].weight for every item of the list, as it is gathering the items of the list.
  • ask_object_type: a boolean value, initially set to False. This is used when you want to build a list of objects of diverse types. When ask_object_type is True, then when items are added to the list, docassemble will seek out a definition of the new_object_type attribute before adding an item to the list. When it gets the object type, the object it adds to the list will be of this type.
  • new_object_type: this works like object_type, except the attribute is undefined and a definition is sought every time an object is added to the list. It is used in conjunction with the ask_object_type attribute.

For more information about gathering items using DAList objects, see the section on groups.

DADict

A DADict acts like a Python dictionary except that dictionary keys and values can be defined through docassemble questions.

To add a value that is a new docassemble object, you need to call the initializeObject() method.

For example:

modules:
  - docassemble.base.core
---
objects:
  - player: DADict
---
mandatory: True
code: |
  player.initializeObject('trustee', DAObject)
  player.initializeObject('beneficiary', DAObject)
  player.initializeObject('grantor', DAObject)
  player.gathered = True
---
mandatory: True
question: The players
subquestion: |
  % for type in player:
  ${ player[type].firstname }
  ${ player[type].lastname } is here.

  % endfor
---
generic object: DAObject
question: |
  What is
  ${ x[i].object_possessive('name') }?
fields:
  - First Name: x[i].firstname
  - Last Name: x[i].lastname
dadict

The first parameter is the name of the attribute. The second parameter is the type of object. The result of using() can be used in place of the class name.

The DADict also uses a similar method called .new(). This method initializes a new object and makes it an entry in the dictionary. For example, if the dictionary is called positions, calling positions.new('file clerk', 'supervisor') will result in the creation of the object positions['file clerk'] and the object positions['supervisor']. The type of object is given by the object_type attribute, or DAObject if object_type is not set. You can also pass a list and it will unpack the list, initializing dictionary entries for each value.

DADicts use the same attributes that DALists use. It also uses:

  • new_item_name: a text value, initially undefined, indicating the key of a new item being gathered into the dictionary.
  • new_item_value: a value, initially undefined, indicating the value of a new item being gathered into the dictionary. This is only used when the .object_type of the DADict is not set.

DADicts use the same methods that DALists use, except for .appendObject(), .append(), .remove(), .discard(), .extend(), .first(), and .last().

It also uses the following methods, which correspond with the same methods of the Python dict.

  • keys()
  • values()
  • update()
  • pop()
  • popitem()
  • setdefault()
  • get()
  • copy()
  • has_key()
  • items()
  • iteritems()
  • iterkeys()
  • itervalues()

For most purposes, your code can treat a DADict object just like a Python dictionary.

objects:
  things: DADict
---
code: |
  if 'abc' in things:
    gathered_abc = True
  else:
    gathered_abc = False
---
code: |
  if len(things) > 0:
    there_are_things = True
  else:
    there_are_things = False
---
mandatory: True
code: |
  things['abc'] = some_variable
---
code: |
  description = ''
  for key, value in things.iteritems():
    description += "* " + key + ": " + value + "\n"

The DADict object also uses some methods that are unique to it. The all_true() and all_false() methods are useful when working with checkbox groups. If you use datatype: checkboxes to set a variable called choices, then choices will be a DADict object.

If you call choices.all_true(), the result will be True if all of the checkboxes were selected (i.e., all of the values of the dictionary are True). If you call choices.any_false(), the result is the opposite of choices.all_true().

If you call choices.all_false(), the result will be True if none of the values were selected (i.e., all of the values of the dictionary are False). If you call choices.any_true(), the result is the opposite of choices.all_false().

You can also call these methods with parameters. Calling choices.all_true('red', 'green') will return True if the user selected the choices for 'red' and 'green'. Calling choices.all_true('red', 'green', exclusive=True) will return True if 'red' and 'green' were selected and these were the only choices selected.

Here is an example that illustrates uses of .all_true():

question: |
  Please tell me what you think.
fields:
  - "Select the fruits you like": likes_fruit
    datatype: checkboxes
    choices:
      - Apples
      - Peaches
      - Pears
      - Plums
---
mandatory: True
question: |
  Summary of your answer
subquestion: |
  It is
  % if likes_fruit.all_true():
  true
  % else:
  not true
  % endif
  that you like all fruit.
  
  It is
  % if likes_fruit.all_true('Apples', 'Pears'):
  true
  % else:
  not true
  % endif
  that you like apples and pears.

  It is
  % if likes_fruit.all_true('Apples', 'Pears', exclusive=True):
  true
  % else:
  not true
  % endif
  that apples and pears are the only fruits you like.

  It is
  % if likes_fruit.any_true():
  true
  % else:
  not true
  % endif
  that you like at least one fruit.
all-true

Here is an example that uses .all_false():

question: |
  Please tell me what you think.
fields:
  - "Select the fruits you like": likes_fruit
    datatype: checkboxes
    choices:
      - Apples
      - Peaches
      - Pears
      - Plums
---
mandatory: True
question: |
  Summary of your answer
subquestion: |
  It is
  % if likes_fruit.all_false():
  true
  % else:
  not true
  % endif
  that you don't like any of the fruit.
  
  It is
  % if likes_fruit.all_false('Apples', 'Pears'):
  true
  % else:
  not true
  % endif
  that you do not like apples or pears.

  It is
  % if likes_fruit.all_false('Apples', 'Pears', exclusive=True):
  true
  % else:
  not true
  % endif
  that apples and pears are the only fruits you do not like.

  It is
  % if likes_fruit.any_false():
  true
  % else:
  not true
  % endif
  that there is at least one fruit you do not like.
all-false

The method .true_values() will return a list of keys that are True.

question: |
  Please tell me what you think.
fields:
  - "Select the fruits you like": likes_fruit
    datatype: checkboxes
    choices:
      - Apples
      - Peaches
      - Pears
      - Plums
---
mandatory: True
question: |
  Summary of your answer
subquestion: |
  You like
  ${ likes_fruit.true_values() }.
true-values

Similarly, .false_values() will return a list of keys that are False.

question: |
  Please tell me what you think.
fields:
  - "Select the fruits you like": likes_fruit
    datatype: checkboxes
    choices:
      - Apples
      - Peaches
      - Pears
      - Plums
---
mandatory: True
question: |
  Summary of your answer
subquestion: |
  You do not like
  ${ likes_fruit.false_values() }.
false-values

For more information about using checkboxes, see the documentation for checkbox groups.

Like the DAList, the DADict supports the method item(). If Fruit is a DADict, fruit.item('apple') is equivalent to fruit['apple'], except that if fruit does not have an item 'apple', the result will be empty text. This is a helpful method in some contexts, such as fillable PDF forms. Empty text will also result if you try to use attributes on the result, so fruit.item('apple').seeds or fruit.item('apple').total_value() will also result in empty text.

code: |
  pet_types = [
    'dog', 'cat', 'goldfish',
    'turtle', 'lizard', 'rabbit',
    'rock', 'hamster', 'gerbil',
    'rat']
---
question: |
  What type of pet do you have?
fields:
  - Type: pet.new_item_name
    datatype: dropdown
    code: pet_types
---
question: |
  Describe your pet ${ i }.
fields:
  - Name: pet[i].name
  - Color: pet[i].color
  - Legs: pet[i].legs
    datatype: integer
---
question: |
  Do you have any other pets?
yesno: pet.there_is_another
---
mandatory: True
question: |
  Description of ten pets.
subquestion: |
  % for pet_type in pet_types:
  The name of your pet ${ pet_type }
  is ${ pet.item(pet_type).name }.
  It is ${ pet.item(pet_type).color }
  and it has
  ${ pet.item(pet_type).legs } legs.
  
  % endfor
item-dict

For more information about using DADict objects, see the section on groups.

DASet

A DASet is like a DADict and a DAList, except it acts like a Pythonset.”

objects:
  issues: DASet
---
code: |
  if user_needs_to_apply:
    issues.add('application')

DASets use the same methods that DALists use, except for .appendObject(), .append(), .extend(), .first(), and .last(). It also uses the following methods, which correspond with the methods of the Python set.

  • add()
  • copy()
  • discard()
  • difference()
  • intersection()
  • isdisjoint()
  • issubset()
  • issuperset()
  • pop()
  • remove()
  • union()

For more information about using DASet objects, see the section on groups.

DAFile

A DAFile object is used to refer to a file, which might be an uploaded file, an assembled document, or a file generated by code. It has the following attributes:

  • filename: the filename (e.g., complaint.pdf).
  • mimetype: the MIME type of the file.
  • extension: the file extension (e.g., pdf or rtf).
  • number: the internal integer number used by docassemble to keep track of documents stored in the system. (You will likely never need to use this.)
  • ok: this is True if the number has been defined, and is otherwise False. (You will likely never need to use this, either.)

You might work with DAFile objects in the following contexts:

  • Your interview contains a document upload field. The variable representing the upload will be set to a DAFileList object after the upload has been done. If the variable name is pretty_picture, then pretty_picture[0] will be a DAFile object.
  • Your interview assembles a document and the document is assigned to a variable name. If the variable name is motion_to_reconsider, then motion_to_reconsider will be a DAFileCollection object, the attributes of which represent the various formats of the document. For example, motion_to_reconsider.pdf (the .pdf here is an attribute, not a file extension) will be a DAFile object representing the PDF version of the document.
  • Your interview contains code that needs to create a file. You can use an objects block to create a blank DAFile object. Then you would call .initialize() to give the file a name and a presence on the file system.

You can call .path() on a DAFile object to get the actual file path. Using the file path, can manipulate the underlying file directly in whatever way you want. However, the DAFile object has a number of built-in methods for doing common things with files, so it is a good idea to use the methods whenever possible.

While the DAFile object is saved in your interview dictionary like any other variable, the content of the file may be stored on Amazon S3, Azure blob storage, or the file system, depending on the server’s configuration. The path you obtain from .path() might be different from one screen of your interview to another. You should not save the path to a variable and expect to be able to use that variable across screens of the interview. Rather, you should always access the file through the DAFile object, using its built-in methods (such as .path()). These methods contain code that automatically accounts for the fact that the file might be stored in the cloud. For example, if you use Amazon S3, then when you call .path()), this will cause the file to be retrieved from Amazon S3 and placed into a temporary directory.

The methods of DAFile are the following:

The .initialize() method transforms a fresh, uninitialized DAFile object (e.g., a DAFile object created by the objects block) into an object that can actually be used as a file. The method takes the optional keyword parameters filename or extension. The .initialize() method can be used as follows (where myfile is a DAFile object):

  • myfile.initialize(filename='image.jpg') - filename will be image.jpg, extension will be jpg, mimetype will be image/jpeg.
  • myfile.initialize(extension='jpg') - filename will be file.jpg, extension will be jpg, mimetype will be image/jpeg.
  • myfile.initialize() - filename will be file.txt, extension will be txt, mimetype will be text/plain.

If the object has already been initialized, the .initialize() method can safely be called on it, but this will only have the effect of calling .retrieve() on it, and the filename and extension parameters will not overwrite existing values.

The following example uses the Python Imaging Library to create a JPEG image.

modules:
  - docassemble.base.core
---
imports:
  - PIL
---
objects:
  - myfile: DAFile
---
mandatory: True
code: |
  myfile.initialize(filename="foo.jpg")
  im = PIL.Image.new("RGB",
                     (512, 512),
                     "green")
  im.save(myfile.path())
---
mandatory: True
question: |
  Here is the picture.
subquestion: |
  ${ myfile }
dafile

The .show() method returns markup that displays the file as an image. This method takes an optional keyword argument, width, which can be set to, e.g., '1in', '44mm', or '20pt'. See inserting images for more information about this markup.

In the context of a Mako template, writing ${ myfile } is equivalent to writing ${ myfile.show() } (where myfile is a DAFile object).

The .path() method returns a complete file path that you can use to read the file or write to the file.

The .num_pages() method returns the number of pages in a PDF file. If the file is not a PDF file, it returns 1.

The .url_for() method returns a URL at which the file can be accessed. The URL should only be used in the context of the user’s session and the user’s web browser. For example, if you are using cloud storage as your form of data storage, the URL will link directly to the cloud and will expire after an hour. If you are not using cloud storage, the server will only allow access to the file to the current user.

mandatory: True
question: |
  This interview is all done.
subquestion: |
  To start your case,
  [download your pleading],
  print it, and take it
  to clerk's office at the
  court.

  [download your pleading]: ${ complaint.pdf.url_for() }
---
attachment:
  filename: complaint
  name: Complaint
  variable name: complaint
  content: |
    This is a complaint.

    Blah, blah, blah
dafile-url-for

However, if you have set the private attribute to False by calling .set_attributes(private=False) on the object, than the URL obtained from .url_for() will be accessible to anyone, regardless of whether they are logged in. For more information, see .set_attributes().

The .url_for() method can also be used to generate a temporary URL where anyone who knows the URL can access the file, regardless of whether they are logged in, regardless of whether the server uses cloud data storage, and regardless of whether the private attribute is True or False. To obtain such a URL, include temporary=True as a keyword parameter. By default, the URL will expire after 30 seconds. To extend this time, include the optional keyword parameter seconds. This example creates a URL that expires in 60 seconds.

mandatory: True
question: |
  This interview is all done.
subquestion: |
  To start your case,
  [download your pleading]
  in the next sixty seconds,
  print it, and take it
  to clerk's office at the
  court.

  [download your pleading]: ${ complaint.pdf.url_for(temporary=True, seconds=60) }
---
attachment:
  filename: complaint
  name: Complaint
  variable name: complaint
  content: |
    This is a complaint.

    Blah, blah, blah
dafile-url-for-temporary

The .retrieve() command ensures that a stored file is ready for use on the system. Calling .retrieve is necessary because if docassemble is configured to use Amazon S3 or Azure blob storage, documents are stored in the cloud, and the server accesses them by copying them from the cloud to the server and then copying them back to the cloud. If the file does not exist yet, calling .retrieve() will generate an error.

The .set_attributes() command allows you to set two characteristics of the uploaded document:

  • private: the default value of this attribute is True, which means that other interviews and other interview sessions cannot access the contents of the file, even if they know the .number, or have the DAFile object itself, or have a URL to the file obtained from .url_for(). You will need to set the private attribute to False if you want other sessions or other people to be able to access the file. For example, you might store a DAFile object in storage and retrieve it within other interviews at a later time. The contents of the file will not be accessible unless you set private to False.
  • persistent: the default value of this attribute is False, which means that the file will be deleted when the interview session is deleted. Interview sessions are deleted when the user presses an exit button or an exit command is run. Interview sessions are also deleted when the session has been inactive for a period. You can prevent the deletion of a file by setting the persistent attribute to True.

You can set these attributes with code like this:

question: |
  Sign your name
signature: user_signature
---
mandatory: True
code: |
  user_signature.set_attributes(persistent=True)

To read the values of the attributes for a variable like user_signature, refer to user_signature.private and user_signature.persistent, which are set by .retrieve(). Setting these attributes directly has no effect; you need to use .set_attributes() to set them.

The .slurp() method reads the contents of the file and returns them as a text value.

contents = the_file.slurp()

By default, the .slurp() method attempts to automatically decode text files using the utf8 encoding. To turn off this automatic decoding feature, call it with .slurp(auto_decode=False).

The .readlines() method reads the contents of the file, line-by-line, and returns the lines as a list.

for line in the_file.readlines():
  if line.startswith('header'):
    header_line = line

The .set_mimetype() method sets the .mimetype and .extension attributes based on the given MIME type.

the_file.set_mimetype('image/jpeg')

The .write() method takes a variable as an argument and writes the contents of the variable to the file.

contents = the_file.slurp()
contents = re.sub(r'swords', r'ploughshares', contents)
the_file.write(contents)

The .copy_into() method overwrites any existing contents of the file with the contents of the file given as an argument.

the_file.copy_into(other_file)

The other_file can be a path to a file on the system. If other_file is a DAFile, DAFileList, DAFileCollection, or DAStaticFile, the file at other_file.path() will be used.

The .from_url() method overwrites any existing contents of the file with the contents of the given URL.

the_file.from_url("https://example.com/file.pdf")

In order to initialize a DAFile with contents from a remote source so that it can be used in an interview, first declare the object:

objects:
  - pdf_file: DAFile

Then use code to initialize the object and set the contents:

code: |
  pdf_file.initialize(extension="pdf")
  pdf_file.from_url("https://example.com/the_file.pdf") 

The .commit() method ensures that changes to the file are stored permanently. Under normal circumstances, docassemble will automatically commit changes when the interview is finished processing (i.e. right before a new screen appears), but .commit() can be called to ensure that changes are written, just in case there is an error.

the_file.commit()

The .make_pngs() method can be used on PDF files. It creates PNG versions of the pages of the document. In most circumstances, you will not need to use this function, because when a user uploads a file, a process is started whereby each page of the PDF file is converted to PNG. However, if that process is not started for whatever reason, for example if you are constructing files manually, you can start the process by running .make_pngs(). This will launch background processes and wait until they are completed.

DAFileCollection

DAFileCollection objects are created internally by docassemble in order to refer to a document assembled by an attachment/attachments block. When such a block features a variable name, then a variable by that name will be defined as a DAFileCollection object. The object is called a “collection” because an attachment can have multiple formats: PDF, RTF, etc.

A DAFileCollection object has attributes for each file type generated (e.g., pdf or rtf), where the attributes are objects of type DAFile.

For example, if the variable my_file is a DAFileCollection, my_file.pdf will be a DAFile containing the PDF version, and my_file.rtf will be a DAFile containing the RTF version.

In addition, each DAFileCollection object also has an attribute .info containing information about the attachment, such as the name, the filename (before an extension is added), and a description. The .info attribute is a dictionary containing the following keys:

  • name is the printable name of the document
  • filename is the base name used to generate the filename. If filename is custody_complaint, the PDF file will be custody_complaint.pdf.
  • description is the description of the attachment.

The DAFileCollection also has some methods so that you can use it much as you would use a DAFile.

The .url_for() method returns a URL to the first document type in the collection. By default, this is the PDF version, but this can be changed with the valid formats modifier.

The .path() method returns a complete file path that you can use to access the first document type in the collection.

The .num_pages() method returns the total number of pages in the PDF file. If there is no PDF file, it returns 1.

The .show() method inserts markup that displays each file in the collection as an image, or as a link if the file cannot be displayed as an image. This method takes an optional keyword argument, width.

DAFileList

A DAFileList is a DAList, the items of which are expected to be DAFile objects.

When a question has a field with a datatype for a file upload (datatype: file and datatype: files), the variable will be defined as a DAFileList object containing the file or files uploaded. These variables can be used in much the same way that DAFile variables can be used.

The .show() method inserts markup that displays each file as an image. This method takes an optional keyword argument, width.

When included in a Mako template, a DAFileList object will effectively call show() on itself.

The .url_for() method returns a URL at which the first file in the list can be accessed. This is useful when working with DAFileList objects returned from datatype: file, when you know that the list will only have one element in it.

The .path() method returns a complete file path that you can use to access the first file in the collection.

The .num_pages() method returns the total number of pages in all PDF files in the list. If a file is not a PDF file, it counts as 1 page.

The .set_attributes() method calls .set_attributes() on each of the DAFiles in the list, applying the same attributes to each file. For an explanation of how this method works, see its documentation.

You would call the method like this:

question: |
  Upload a file
fields:
  - File: the_upload
    datatype: file
---
mandatory: True
code: |
  the_upload.set_attributes(private=False)

DAStaticFile

A DAStaticFile represents a file in the “static folder” of a package. It has some of the same characteristics and methods of a DAFile.

It depends on one attribute, filename, which should be a reference to a static file, such as:

  • coins.png - a file in the static folder of the current package
  • docassemble.base:data/static/cow.jpg - a file in the static folder of another package.

The DAStaticFile object can be used like this:

objects:
  - the_icon: DAStaticFile
---
code: |
  if user.gender == 'Female':
    the_icon.filename = 'female243.png'
  else:
    the_icon.filename = 'male244.png'
---
mandatory: True
question: |
  The image
  ${ the_icon.show(width='1em') }
subquestion: |
  ${ the_icon }
static-file

It can also be initialized like this:

objects:
  - the_icon: DAStaticFile.using(filename='coins.png')

The .show() method inserts markup that displays the file as an image. This method takes an optional keyword argument, width.

When included in a Mako template, a DAStaticFile object will effectively call show() on itself.

The .slurp() method reads the contents of the file and returns them as a text value.

contents = the_file.slurp()

By default, the .slurp() method attempts to automatically decode text files using the utf8 encoding. To turn off this automatic decoding feature, call it with .slurp(auto_decode=False).

The .url_for() method returns a URL at which the file can be accessed.

The .path() method returns a complete file path that you can use to access the file on the server.

Here is an example that shows how DAStaticFile, DAFileCollection, DAFileList, and DAFile objects can be used interchangeably.

objects:
  - static_file: DAStaticFile.using(filename='cow.jpg')
---
attachment:
  variable name: assembled_file
  filename: assembled_file
  content: |
    Hello world!
---
question: |
  Upload a file
fields:
  - File: uploaded_file
    datatype: file
---
question: |
  What type of file do you want?
choices:
  Static:
    code: |
      the_file = static_file
  Assembled:
    code: |
      the_file = assembled_file
  Upload:
    code: |
      the_file = uploaded_file
  Single:
    code: |
      the_file = uploaded_file[0]
---
code: |
  email_sent = send_email(to=email_address,
    body="Here is your document",
    subject="Your document",
    attachments=the_file)
---
code: |
  (the_path, the_mimetype) = path_and_mimetype(the_file)
---
code: |
  button_list = [{True: 'Next', 'image': the_file}]
---
field: menu_selection
question: |
  Your document
subquestion: |
  ### As image
  ${ the_file.show(width='1em') }

  ${ the_file }

  ### As path
  
  `${ the_file.path() }`

  ### As url
  
  `${ the_file.url_for() }`

  ### As external url
  
  `${ the_file.url_for(_external=True) }`

  ### From url_of()

  `${ url_of(the_file) }`

  ### From path_and_mimetype()

  `${ the_path }`

  `${ the_mimetype }`
buttons:
  code: button_list
---
mandatory: True
need:
  - menu_selection
question: |
  All done
buttons:
  - Restart: restart
file-types

DAEmail

The e-mail receiving feature converts actual e-mails into objects of type DAEmail. These objects have the following attributes:

  • short: the code that was assigned by interview_email() (e.g. ugjrye) in order to create the e-mail address to which this e-mail was sent (e.g. [email protected]).
  • key: the key that was passed to interview_email(), or None if no key was passed.
  • index: the index that was passed to interview_email(), or None if no index was passed.
  • address_owner: the e-mail address of the user whose identity and privileges were being used when interview_email() was called. If the user was not logged in when interview_email() was called, this will be None.
  • to_address: a DAEmailRecipientList object representing the recipients of the e-mail.
  • cc_address: a DAEmailRecipientList object representing the “carbon copy” recipients of the e-mail.
  • from_address: a DAEmailRecipient object representing the sender of the e-mail.
  • reply_to: a DAEmailRecipient object representing the the Reply-to header of the e-mail.
  • return_path: a DAEmailRecipient object representing the the Return-path header of the e-mail.
  • subject: the subject line of the e-mail.
  • datetime_message: a datetime object representing the stated date and time of the e-mail message.
  • datetime_received: a datetime object representing the actual date and time of the message.
  • body_text: a DAFile object referring to a file containing the plain text version of the e-mail. If the e-mail did not contain a plain text version, body_text will be None.
  • body_html: a DAFile object referring to a file containing the HTML version of the e-mail. If the e-mail did not contain a HTML version, body_html will be None.
  • attachment: an object of type DAFileList containing any files that were attached to the e-mail. Each attachment is a DAFile object.
  • headers: a DAFile object referring to a file containing a JSON representation of the headers of the e-mail. The format of the JSON file is a list, where each item in the list is a list with two items, the first of which is the name of the header (e.g., To, From), and the second item is the value.

DAEmailRecipient

A DAEmailRecipient object is used within DAEmail objects to represent a single e-mail address and the name associated with the e-mail address.

It has two attributes:

  • address: the e-mail address (e.g., [email protected]).
  • name: the name of the owner of the address (e.g., Fred Smith).

.email_address()

If recipient is a DAEmailRecipient, then calling recipient.email_address() will return the person’s name followed by the person’s e-mail address, in the standard e-mail format. E.g., 'Fred Smith <[email protected]>'. If the name is not defined, it will simply return the e-mail address ([email protected]).

You can suppress the inclusion of the person’s name by setting the optional keyword parameter include_name to False.

This method is intended to allow you to use DAEmailRecipient objects in much the same way as Person objects are used when sending e-mails with send_email(). (See the .email_address() method for Person objects).

.exists()

The .exists() method returns True if the .address attribute has been defined, and False otherwise.

DAEmailRecipientList

A DAEmailRecipientList is a DAList of DAEmailRecipient objects.

DATemplate

The template block allows you to store some text to a variable. See template. The variable will be defined as an object of the DATemplate class.

Objects of this type have two attributes:

  • content
  • subject

When docassemble defines a template, it assembles any Mako in the content and optional subject attributes as the resulting text. Note that the text may have Markdown markup in it.

If a template is a variable disclaimer, the content can be inserted by writing ${ disclaimer }, ${ disclaimer.content }, or ${ disclaimer.show() }. The latter method facilitates the use of DATemplates and DAFiles interchangably.

For more information about using DATemplates, see the documentation for templates. Also, see the documentation for send_email() and send_sms().

DAEmpty

The DAEmpty object is designed to stand in place of an object that might otherwise have important attributes, but it will always return an empty string whenever the interview tries to access its attributes.

So if exemption is a DAEmpty object, exemption.amount will return '', as will exemption[2], exemption[4].authority.address.unit, etc.

This object is used internally by the item() method. It can also be useful to use the DAEmpty object if you have a template that refers to variables that you decide you don’t actually want to use. If your interview simply sets those variables to DAEmpty, your template will not trigger an error.

The DALink class represents a hyperlink to a URL. It has two properties:

  • url: the URL to which the link points.
  • anchor_text: the text of the link that the user sees.

In most circumstances, it is not necessary to use a DALink to represent a hyperlink because you can use Markdown to indicate a hyperlink. However, when you are creating a document from a docx template file, Markdown syntax is not available. When a DALink object is used within a docx template file, an actual .docx hyperlink is inserted into the document.

objects:
  - the_link: DALink
---
question: |
  Tell me about a hyperlink.
fields:
  - URL: the_link.url
    hint: "https://google.com"
  - Anchor text: the_link.anchor_text
    hint: Google
---
mandatory: True
question: |
  Hyperlinks
subquestion: |
  You can find all the information
  you need at ${ the_link }.
attachment:
  name: Hyperlink demonstration
  filename: hyperlink_demo
  docx template file: dalink.docx
dalink

DARedis

The DARedis class facilitates the use of Redis for in-memory storage.

objects:
  r: DARedis
---
mandatory: True
code: |
  key = r.key('my_variable')
  r.set(key, 'hello world')
---
mandatory: True
question: |
  Value has been retrieved from
  the redis server.
subquestion: |
  The key is `${ key }`.
  
  The value is "${ r.get(key) }".
redis

For the most part, an object of type DARedis functions just like an object created through redis.StrictRedis() using the standard Python package called redis. You can use methods like .set(), .get(), .delete(), .incr(), etc.

However, there are three additional methods that facilitate the use of Redis in the context of docassemble interviews.

The key() method is a convenience function for obtaining keys that you can use as Redis keys in order to avoid name collisions across interviews. Given a key like 'favorite_fruit', it returns the key with a prefix based on the interview, like docassemble.food:data/questions/fruit.yml:favorite_fruit. You could use favorite_fruit as a key, but if another interview on the system used the same key, the interviews would interfere with one another.

The set_data() and get_data() methods act just like the standard methods set() and get(), except that they perform pickling and unpickling. This allows you to store and retrieve docassemble objects or any type of data structure that is able to be pickled. The set_data() method takes an optional keyword argument expire, which you can set to an integer representing the number of seconds after which the data should be removed from Redis.

objects:
  - r: DARedis
---
question: |
  Upload a file.
subquestion: |
  Note: if someone else uses this
  interview in the 30 seconds
  after you upload the file,
  your file will be shown to
  that user.
fields:
  - File: uploaded_file
    datatype: file
---
code: |
  mykey = r.key('upload')
---
code: |
  file_from_redis = r.get_data(mykey)
---
mandatory: True
code: |
  if file_from_redis is None:
    uploaded_file.set_attributes(private=False)
    r.set_data(mykey, uploaded_file, expire=30)
    del file_from_redis
comment: |
  If there is no file in Redis, we
  get the user to upload a file.
  Then we undefine file_from_redis
  (which is None) so that in the
  next question, the interview will
  try to fetch the file from Redis
  again.
---
mandatory: True
question: |
  The file
subquestion: |
  ${ file_from_redis }
redis-data

DACloudStorage

The DACloudStorage object allows you to access low-level functionality of cloud storage using Amazon S3 or Azure blob storage, using the boto3 and azure.storage.blob libraries, respectively.

Suppose you include the following in your interview:

objects:
  - cloud: DACloudStorage

If you have enabled s3 in your Configuration, then:

  • cloud.conn will return a boto3.resource('s3') object initialized according to the s3 configuration.
  • cloud.client will return a boto3.client('s3') object initialized according to the s3 configuration.
  • cloud.bucket will return a Bucket object for the bucket defined in the s3 configuration.
  • cloud.bucket_name will return the name of the bucket defined in the s3 configuration.

If you have enabled azure in your Configuration, then:

  • cloud.conn will return a BlockBlobService() object initialized according to the azure configuration.
  • cloud.container_name will return the name of the container defined in the azure configuration.

In some circumstances, you might not be using s3 or azure for persistent storage, or you may wish to access a different bucket or container. In that case, you can initialize the DACloudStorage object so that it uses a different directive in the Configuration.

For example, if you have an S3 bucket called mybucket-example-com, and your Configuration contains the following:

mybucket:
  access key id: AGJBRKYM3T4FY7HYWNBQ
  secret access key: BkwEQeg+yeC3EJ2MoCDwY8jbiWrtKdLf4q3++EBd
  bucket: mybucket-example-com

Then you can initialize a DACloudStorage object as follows:

objects:
  - cloud: DACloudStorage.using(provider='s3', config='mybucket')

Then, you can use the cloud object to access the contents of the mybucket-example-com bucket in your interview. For example:

question: |
  How to seek help
subquestion: |
  ${ cloud.bucket.Object('markdown_files/help.md').get()['Body'].read().decode('utf-8') }

In this example, the subquestion incorporates the contents of a file called help.md located in the markdown_files folder of the mybucket-example-com bucket.

For more information on how to use these objects, see the documentation for boto3 and azure.storage.blob.

The DACloudStorage object simply provides a convenient way to obtain an authenticated API connection to Amazon S3 or Azure blob storage. If you do not use a DACloudStorage object, you can still use the boto3 and azure.storage.blob packages; the only added complication is that you have to handle authentication yourself. You can use the get_config() function to retrieve custom values from your Configuration.

For example, here is a Python module that defines a function that retrieves a list of object names from an existing S3 bucket.

import boto3
import docassemble.base.util

s3_config = docassemble.base.util.get_config('manual s3 configuration')

def list_keys(prefix):
    conn = boto3.resource('s3', region_name='us-east-1', aws_access_key_id=s3_config['access key id'], aws_secret_access_key=s3_config['secret access key'])
    client = boto3.client('s3', region_name='us-east-1', aws_access_key_id=s3_config['access key id'], aws_secret_access_key=s3_config['secret access key'])
    bucket = conn.Bucket('example-com-data-bucket')
    output = list()
    for item in bucket.objects.filter(Prefix=prefix, Delimiter='/'):
        output.append(item.key)
    return output

This assumes you have a custom directive in your Configuration that looks like this:

manual s3 configuration:
  access key id: FWIEJFIJIDGISEJFWOEF
  secret access key: RGERG34eeeg3agwetTR0+wewWAWEFererNRERERG

DAGoogleAPI

The DAGoogleAPI object provides convenient access to Google’s APIs through a Google service account that you set up in the Google Developers Console and enable in the docassemble Configuration.

The DAGoogleAPI object can be used with the low-level Google API and also with higher-level API packages like gspread or the Cloud Translation API Client Library.

The benefit of the DAGoogleAPI object is that it streamlines the process of authenticating to Google’s servers. It also provides a standard way to keep service account authentication information in the Configuration.

Setup process

In order for your site to communicate with Google, you will need to create an account on the Google Developers Console and create an “app.” Within this app, you will need to create a service account. Then set the service account credentials directive in the Configuration to the credentials for this service account. For more information on how to do this, see the documentation for the service account credentials directive.

Finally, you need to use the Google Developers Console to enable the APIs that you want to use. For example, if you want to use the Google Drive API, you need to explicitly enable the Google Drive API for your app.

Usage

Suppose you define api to be a DAGoogleAPI object:

objects:
  api: DAGoogleAPI

There are two categories of methods available. The first is for the low-level Google API available through the google-api-python-client package. This can be used to control any of the Google APIs.

The scope can be a single scope like 'https://www.googleapis.com/auth/drive', or it can be a list of scopes, like ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/cloud-vision']. You can browse Google’s directory of scopes.

Here is an example of a Python module that uses the .drive_service() method of the DAGoogleAPI object to provide convenience functions for reading and writing to a Google Drive folder:

from docassemble.base.util import DAGoogleAPI, DAFile
import apiclient

api = DAGoogleAPI()

__all__ = ['get_folder_names', 'get_files_in_folder', 'write_file_to_folder', 'download_file']

def get_folder_names():
    service = api.drive_service()
    items = list()
    while True:
        response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and sharedWithMe").execute()
        for the_file in response.get('files', []):
            items.append(the_file)
        page_token = response.get('nextPageToken', None)
        if page_token is None:
            break
    return [item['name'] for item in items]

def get_folder_id(folder_name):
    service = api.drive_service()
    response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and sharedWithMe and name='" + unicode(folder_name) + "'").execute()
    folder_id = None
    for item in response.get('files', []):
        folder_id = item['id']
    return folder_id

def get_file_id(filename, folder_name):
    folder_id = get_folder_id(folder_name)
    if folder_id is None:
        raise Exception("The folder was not found")
    service = api.drive_service()
    file_id = None
    response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType!='application/vnd.google-apps.folder' and '" + str(folder_id) + "' in parents and name='" + unicode(filename) + "'").execute()
    for item in response.get('files', []):
        file_id = item['id']
    return file_id

def get_files_in_folder(folder_name):
    folder_id = get_folder_id(folder_name)
    if folder_id is None:
        raise Exception("The folder was not found")
    service = api.drive_service()
    items = list()
    while True:
        response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType!='application/vnd.google-apps.folder' and trashed=false and '" + str(folder_id) + "' in parents").execute()
        for the_file in response.get('files', []):
            items.append(the_file)
        page_token = response.get('nextPageToken', None)
        if page_token is None:
            break
    return [item['name'] for item in items]

def write_file_to_folder(path, mimetype, filename, folder_name):
    folder_id = get_folder_id(folder_name)
    if folder_id is None:
        raise Exception("The folder was not found")
    service = api.drive_service()
    file_metadata = { 'name': filename, 'parents': [folder_id] }
    media = apiclient.http.MediaFileUpload(path, mimetype=mimetype)
    the_new_file = service.files().create(body=file_metadata,
                                          media_body=media,
                                          fields='id').execute()
    return the_new_file.get('id')

def download_file(filename, folder_name):
    file_id = get_file_id(filename, folder_name)
    if file_id is None:
        raise Exception("The file was not found")
    the_file = DAFile()
    the_file.set_random_instance_name()
    the_file.initialize(filename=filename)
    service = api.drive_service()
    with open(the_file.path(), 'wb') as fh:
        response = service.files().get_media(fileId=file_id)
        downloader = apiclient.http.MediaIoBaseDownload(fh, response)
        done = False
        while done is False:
            status, done = downloader.next_chunk()
    the_file.commit()
    return the_file

To use this module, log in to Google Drive using a normal Google account and create a folder. Then share that folder with the e-mail address of your service account. Then, when you call get_folder_names(), the name of this folder will be part of the list that is returned.

Here is an interview that uses this module to access a Google Drive folder called “DADemo”. It assumes the module is a file called google_drive.py in the same package as the interview.

modules:
  - docassemble.base.util
  - .google_drive
---
mandatory: True
code: |
  first_screen
  files_copied_to_google_drive
  final_screen
---
question: |
  Files in your Google Drive
subquestion: |
  % for item in get_files_in_folder('DADemo'):
  * ${ item } ${ download_file(item, 'DADemo') }
  % endfor
field: first_screen
---
question: |
  Please upload a file.
fields:
  - File: uploaded_files
    datatype: files
---
code: |
  for the_file in uploaded_files:
    (path, mimetype) = path_and_mimetype(the_file)
    write_file_to_folder(path, mimetype, the_file.filename, 'DADemo')
  files_copied_to_google_drive = True
---
event: final_screen
question: Names of files in folder
subquestion: |
  % for item in get_files_in_folder('DADemo'):
  * ${ item }
  % endfor

You can use any Google API, not just the Google Drive API. The .drive_service() method is provided only as a convenience. You can create your own services by running something like:

import apiclient
import docassemble.base.util 

api = docassemble.base.util.DAGoogleAPI()
http = api.http('https://www.googleapis.com/auth/cloud-translation')

service = apiclient.discovery.build('translate', 'v2', http=http)

Note that while the api object can safely persist in your users’ interview answers, a variable like service cannot be pickled, so if you store it in your interview file, you will get an error. Ideally, you should always use Python modules for functions that access APIs, so that there is no danger of an error if docassemble tries to pickle an object that cannot be pickled.

The second category of methods is for Google Cloud packages like google.cloud.storage and google.cloud.translate. If a Google service is available both through google-api-python-client and through one of the Google Cloud packages, the Google Cloud packages are probably preferable because they are newer and easier to use. Most of these packages are not installed in the docassemble system by default and will need to be installed with “Package Management” if you want to use them. Whatever package name Google tells you to use with pip, you can type into the “Package on PyPI” field in “Package Management.”

The methods of the DAGoogleAPI object that can be used with the Google Cloud packages include the following:

  • api.cloud_credentials(scope) - this returns a Credentials object from the google.oauth2.service_account module, initialized for the given scope.
  • api.project_id() - this returns the ID of the “app” in which the service account is configured. This ID, which is just a text string, is required by some API methods.
  • google_cloud_storage_client() - this returns a google.cloud.storage.Client object that can be used to access the Google Cloud Storage API.

Here is an example of a Python module that uses the Google Cloud Storage API (the google.cloud.storage Python package):

import docassemble.base.util

__all__ = ['make_bucket', 'get_bucket', 'make_blob']

api = docassemble.base.util.DAGoogleAPI()
client = api.google_cloud_storage_client()

def make_bucket(bucket_name):
  return client.create_bucket(bucket_name)
  
def get_bucket(bucket_name):
  return client.get_bucket(bucket_name)

def make_blob(bucket_name, blob_name, blob_content):
  bucket = get_bucket(bucket_name)
  blob = bucket.blob(blob_name)
  blob.upload_from_string(blob_content)
  return blob

Classes for information about people and things

Thing

If pet_rock is a Thing, it will be an object with one attribute:

  • pet_rock.name (object of class Name)

If you include ${ pet_rock } in text, the name of the Thing will be inserted. docassemble will look for a definition of pet_rock.name.text.

If you set the name attribute of a Thing to text, the .name.text attribute will be set to the text you provided.

Event

An Event is a type of DAObject with the following attributes initialized by default:

There may be many other attributes you want to use for an Event, but they are up to you to choose.

Person

The Person class encompasses Individuals as well as legal persons, like companies, government agencies, etc. If you create an object of type Person by doing:

objects:
  - opponent: Person

then you will create an object with the following built-in attributes:

Referring to a Person in the context of a Mako template will return the output of .name.full().

If you set the name attribute of a Person to text, the .name.text attribute will be set to the text you provided.

The following attributes are also used, but undefined by default:

The following methods can be used:

.possessive()

Calling defendant.possessive('fish') returns “ABC Corporation’s fish” or “your fish” depending on whether defendant is the user.

.identified()

Calling defendant.identified() returns True if defendant.name.text has been defined. The version for Individuals is different.

.pronoun_objective()

Calling defendant.pronoun_objective() returns “it,” while calling defendant.pronoun_objective(capitalize=True) returns “It.”

.object_possessive()

Calling defendant.object_possessive('fish') returns “defendant’s fish.”

.is_are_you()

Calling defendant.is_are_you() returns “are you” if defendant is the user, and otherwise returns “is defendant.” Calling defendant.is_are_you(capitalize=True) returns “Are you” or “Is defendant.”

.is_user()

Calling defendant.is_user() returns True if the defendant is the user, and otherwise returns False.

.address_block()

Calling defendant.address_block() will return the name followed by the address, in a format suitable for inclusion in a document. For example:

[FLUSHLEFT] ABC Corporation [NEWLINE] 1500 Market Street [NEWLINE] Philadelphia, PA 19102

See markup for more information about how this will appear in documents.

.do_question()

Calling defendant.do_question('testify') returns “do you testify” if the defendant is the user, or otherwise it uses the defendant’s name, as in “does ABC Corporation testify.”

.did_question()

Calling defendant.did_question('testify') returns “did you testify” if the defendant is the user, or otherwise it uses the defendant’s name, as in “did ABC Corporation testify.”

.were_question()

Calling defendant.were_question('guilty') returns “were you guilty” if the defendant is the user, or otherwise it uses the defendant’s name, as in “was ABC Corporation guilty.”

.have_question()

Calling defendant.have_question('lied') returns “have you lied” if the defendant is the user, or otherwise it uses the defendant’s name, as in “has ABC Corporation lied.”

.does_verb()

Calling defendant.does_verb('testify') returns “testify” if the defendant is the user, but otherwise returns “testifies.” The method accepts the optional keyword arguments present and past, which are expected to be set to True or False. For example, defendant.does_verb('testify', past=True) will return “testified.”

.did_verb()

The .did_verb() method is like the .does_verb() method, except that it conjugates the verb into the past tense.

.subject()

The .subject() method returns “you” if the person is the user, but otherwise returns the person’s name.

.email_address()

Calling defendant.email_address() will return the person’s name followed by the person’s e-mail address, in the standard e-mail format. E.g., 'ABC Corporation <[email protected]>'. If the name is not yet defined, the e-mail address by itself ([email protected]) will be returned.

If you want to force docassemble to ask for the recipient’s name, set the optional keyword parameter include_name to True.

You can suppress the inclusion of the person’s name by setting include_name to False.

.sms_number()

Calling defendant.sms_number() will return defendant.mobile_number if the .mobile_number attribute exists; it will not cause the question to be asked. If the .mobile_number attribute does not exist, it will use defendant.phone_number.

The method formats the phone number in E.164 format. It will make use of defendant.country to format the phone number, since the E.164 format contains the international country code of the phone number. If the .country attribute is not defined, the method will call get_country(). The .country attribute is expected to be a two-letter, capitalized abbreviation of a country.

Organization

An Organization is a subclass of Person. It has the attribute .office, which is an object of type OfficeList.

It uses the following attributes, which by default are not defined:

  • handles: refers to a list of problems the organization handles.
  • serves: refers to a list of counties the organization serves.

The .will_handle() method returns True or False depending on whether the organization serves a given county or handles a given problem. It takes two optional keyword arguments: problem and county. For example, you could call agency.will_handle(problem='Divorce', county='Hampshire County').

Individual

The Individual is a subclass of Person. This class should be used for persons who you know are human beings.

If you create an object of type Individual by doing:

objects:
  - president: Individual

then you will create an object with the following built-in attributes:

  • president.name (object of class IndividualName)
  • president.child (object of class ChildList)
  • president.income (object of class Income)
  • president.asset (object of class Asset)
  • president.expense (object of class Expense)

In addition, the following attributes will be defined by virtue of an Individual being a kind of Person:

The following attributes are also used, but undefined by default:

  • age - this can be set to a number. It is used by the age_in_years() method.
  • birthdate - this can be the result of a date field, or a datetime object. It is used by the age_in_years() method if the age attribute has not been defined.
  • gender - this should be set to 'male', 'female', or something else. It is used by a variety of methods such as pronoun().

A number of useful methods can be applied to objects of class Individual. Many of them will respond differently depending on whether the Individual is the user or not. If you use these methods, be sure to inform docassemble who the user is by inserting the following initial block:

initial: True
code: |
  set_info(user=user, role='user_role')

(If you include the basic-questions.yml file, this is done for you.)

.identified()

Returns True if the individual’s name has been defined yet, otherwise it returns False.

.age_in_years()

user.age_in_years() the user’s age in years as a whole number.

There are two optional arguments that modify the method’s behavior:

  • user.age_in_years(decimals=True) returns the user’s age in years with the fractional part included.
  • user.age_in_years(as_of="5/1/2015") returns the user’s age as of a particular date.

To determine the user’s age, this method first looks to see if there is an attribute age. If there is, the value of this attribute is returned. However, the age_in_years() method will not cause the interview to seek out this attribute.

If the age attribute is not defined, this method will calculate the individual’s age based on the birthdate attribute, which will be interpreted as a date. The birthdate attribute can be a date expressed in text, as it would be if it was defined by a date field, or it can be a datetime object.

.first_name_hint() and .last_name_hint()

When you are writing questions in an interview, you may find yourself in this position:

  • You are asking for the name of a person;
  • That person whose name you need may be the user;
  • The user may be logged in;
  • The user, if logged in, may have already provided his or her name on the user profile page; and
  • It would be repetitive for the user to retype his or her name.

In this situation, it would be convenient for the user if the user’s name was auto-filled on the page. The .first_name_hint() and .last_name_hint() methods accomplish this for you. You can ask for an individual’s name as follows:

generic object: Individual
question: |
  What is ${ x.object_possessive('name') }?
fields:
  - First Name: x.name.first
    default: ${ x.first_name_hint() }
  - Middle Name: x.name.middle
    required: False
  - Last Name: x.name.last
    default: ${ x.last_name_hint() }
  - Suffix: x.name.suffix
    required: False
    code: |
      name_suffix()

For an explanation of how .object_possessive() works, see the Person class.

.possessive()

If the individual’s name is “Adam Smith,” this returns “Adam Smith’s.” But if the individual is the current user, this returns “your.”

.salutation()

Depending on the gender attribute, the .salutation() method returns “Mr.” or “Ms.” This can be helpful when writing letters.

template: letter_to_client
content: |
  Dear
  ${ client.salutation() }
  ${ client.name.last }:

  I hope this letter finds you well.

  Blah, blah, blah.

  Dear
  ${ client.salutation(with_name=True) }:

  I hope this letter finds you well.

  Blah, blah, blah.

  Dear
  ${ client.salutation(with_name_and_punctuation=True) }

  I hope this letter finds you well.

  Blah, blah, blah.
salutation

The function takes some optional keyword arguments:

  • client.salutation() returns Mr.
  • client.salutation(with_name=True) returns Mr. Jones
  • client.salutation(with_name_and_punctuation=True) returns Mr. Jones:

This function relies on a few attributes, which it looks for but does not assume exist:

  • If .salutation_to_use is set, .salutation() uses its value as the salutation instead of 'Mr.' or 'Ms.'.
  • If .is_doctor is set to a true value, the salutation “Dr.” is used.
  • If .is_judge is set to a true value, the salutation “Judge” is used.
  • If .name.suffix is 'MD' or 'PhD', the salutation “Dr.” is used.
  • If .name.suffix is 'J', the salutation “Judge” is used.
  • If .is_friendly is set to a true value, a comma will be used in place of a colon when with_name_and_punctuation is true.

The operation of this function can be customized with docassemble.base.util.update_language_function(). Use the function name 'salutation' and provide a function that takes an object of class Individual as an argument. For reference, here is the default function (from docassemble.base.functions):

def salutation_default(indiv, with_name=False, with_name_and_punctuation=False):
    """Returns Mr., Ms., etc. for an individual."""
    used_gender = False
    if hasattr(indiv, 'salutation_to_use') and indiv.salutation_to_use is not None:
        salut = indiv.salutation_to_use
    elif hasattr(indiv, 'is_doctor') and indiv.is_doctor:
        salut = 'Dr.'
    elif hasattr(indiv, 'is_judge') and indiv.is_judge:
        salut = 'Judge'
    elif hasattr(indiv, 'name') and hasattr(indiv.name, 'suffix') and indiv.name.suffix in ('MD', 'PhD'):
        salut = 'Dr.'
    elif hasattr(indiv, 'name') and hasattr(indiv.name, 'suffix') and indiv.name.suffix == 'J':
        salut = 'Judge'
    elif indiv.gender == 'female':
        used_gender = True
        salut = 'Ms.'
    else:
        used_gender = True
        salut = 'Mr.'
    if with_name_and_punctuation or with_name:
        if used_gender and indiv.gender not in ('male', 'female'):
            salut_and_name = indiv.name.full()
        else:
            salut_and_name = salut + ' ' + indiv.name.last
        if with_name_and_punctuation:
            if hasattr(indiv, 'is_friendly') and indiv.is_friendly:
                punct = ','
            else:
                punct = ':'
            return salut_and_name + punct
        elif with_name:
            return salut_and_name
    return salut

If you wanted a simpler function, you could include something like this in a Python module that you include in your interview:

def my_salutation(indiv):
    if indiv.is_powerful:
        return "Your excellency"
    else:
        return "Hey you"

docassemble.base.util.update_language_function('*', 'salutation', my_salutation)

.pronoun_possessive()

If the individual is client, then client.pronoun_possessive('fish') returns “your fish,” “his fish,” or “her fish,” depending on whether client is the user and depending on the value of client.gender. client.pronoun_possessive('fish', capitalize=True) returns “Your fish,” “His fish,” or “Her fish.”

If you want to refer to the individual in the third person even if the individual is the user, write client.pronoun_possessive('fish', third_person=True).

For portability to different languages, this method requires you to provide the noun you are modifying. In some languages, the possessive pronoun may be different depending on what the noun is.

.pronoun()

Returns “you,” “him,” or “her,” depending on whether the individual is the user and depending on the value of the gender attribute. If called with capitalize=True, the word will be capitalized (for use at the beginning of a sentence).

.pronoun_objective()

For the Individual class, pronoun_objective() does the same thing as pronoun. (Other classes returns “it.”) If called with capitalize=True, the output will be capitalized.

.pronoun_subjective()

Returns “you,” “he,” or “she,” depending on whether the individual is the user and depending on the value of the gender attribute.

You can call this method with the following optional keyword arguments:

  • third_person=True: will use “he” or “she” even if the individual is the user.
  • capitalize=True: the output will be capitalized (for use at the beginning of a sentence)

.yourself_or_name()

Returns “yourself” if the individual is the user, but otherwise returns the person’s name. If called with the optional keyword argument capitalize=True, the output will be capitalized.

Name

The Name is the base class for names of things, such as Person. For example, if plaintiff is a Person, plaintiff.name is an object of type Name. If plaintiff is an Individual, plaintiff.name is an object of type IndividualName, which is a subtype of Name. (The IndividualName is defined in the next section.)

Objects of the basic Name class have just one attribute, text. To set the name of a Person called company, for example, you can do something like this:

question: |
  Do you wish to sue ${ company }?
yesno: user_wants_to_sue
name-company-question

There are multiple ways to refer to the name of an object, but the best way is to write something like this:

question: |
  Do you wish to sue ${ company }?
yesno: user_wants_to_sue
name-company

Multiple ways of referring to the name of a Person are illustrated in the following interview:

question: |
  What are you fighting?
field: opponent.name.text
choices:
  - the Empire
  - the Rebel Alliance
---
mandatory: True
question: |
  You are fighting
  ${ opponent.name.full() }.
subquestion: |
  Your enemy is ${ opponent.name }.

  Your opponent is ${ opponent }.
name

Note that ${ opponent.name.full() }, ${ opponent.name }, and ${ opponent } all return the same thing. This is because a Person in the context of a Mako template returns .name.full(), and a Name returns .full().

The reason a name is not just a piece of text, but rather an object with attributes like text and methods like .full(), is that some objects have names with multiple parts that you will want to express in multiple ways. You might have a list of parties in a case, where the parties can be companies or individuals. It helps to have a common way of referring to the names of these objects.

The Name and IndividualName objects support the following methods:

  • .full()
  • .firstlast()
  • .lastfirst()

Applied to an IndividualName object, these methods return different useful expressions of the name. Applied to a Name object, these methods all return the same thing – the .text attribute. This is useful because you can write things like this, which lists the names of the parties in a bullet-point list:

template: client_letter
content: |
  We need to be prepared to bring a lawsuit against the following:

  % for party in enemy:
  * ${ party.name.lastfirst() }
  % endfor
---
objects:
  enemy: PartyList
---
mandatory: True
code: |
  enemy.appendObject(Individual)
  enemy[0].name.first = "Darth"
  enemy[0].name.last = "Vader"
  enemy.appendObject(Person)
  enemy[1].name.text = "Death Star Corporation"
  enemy.appendObject(Individual)
  enemy[2].name.first = "Kylo"
  enemy[2].name.last = "Ren"
  enemy.gathered = True
lastfirst

In this template, the author does not need to worry about which parties are companies and which parties are individuals; the name will be listed in the bullet-point list in an appropriate way. For individuals, the last name will come first, but for non-individuals, the regular name will be printed.

The Name and IndividualName objects also support the method:

  • .defined()

This returns True if the necessary component of the name (.text for a Name, first for an IndividualName) has been defined yet. Otherwise it returns False.

IndividualName

The Individual class is a subclass of Person. It defines the name attribute as an IndividualName rather than a Name. An IndividualName uses the following attributes, which are expected to be text:

  • first
  • middle
  • last
  • suffix

In the context of a Mako template, a reference to an IndividualName on its own will return .full().

The full() method attempts to form a full name from these components. Only first is required, however. This means that if you refer to an IndividualName in a Mako template, e.g., by writing ${ applicant.name }, docassemble will attempt to return applicant.name.full(), and if applicant.name.first has not been defined yet, docassemble will look for a question that defines applicant.name.first.

Here is how full() and other methods of the IndividualName work:

  • applicant.full(): “John Q. Adams”
  • applicant.full(middle="full"): “John Quincy Adams”
  • applicant.firstlast(): “John Adams”
  • applicant.lastfirst(): “Adams, John”
  • applicant.defined(): Returns True if the .first attribute has been defined yet.

The IndividualName also uses the attribute .uses_parts, which is True by default. If .uses_parts is set to False, then the methods of the object fall back on the methods of Name, and the individual’s name is stored in the attribute .text.

If you set the name attribute of an Individual to text, the .uses_parts attribute will be set to False and .name.text will be set to the text you provided.

Address

An Address has the following text attributes:

  • address: e.g., “123 Main Street”
  • unit: e.g., “Suite 100”
  • city: e.g., “Springfield”
  • state: e.g., “MA”
  • zip: e.g. “01199”
  • country: e.g., ‘US’
  • city_only: defaults to False. See City, below.

It also has an attribute location, which is a LatitudeLongitude object representing the GPS coordinates of the address.

If you refer to an address in a Mako template, it returns .block().

The .block() method returns a formatted address. The attribute city is needed.

The .geolocate() method determines the latitude and longitude of the address and stores it in the attribute location, which is a LatitudeLongitude object. It uses the geopy.geocoders.GoogleV3 class. To use this, you will need an API key for the Google Maps Geocoding API, which you will need to add to the configuration as the [google api] subdirective under the google directive.

If you call .geolocate() on an Address object called myaddress, the following attributes will be set:

  • myaddress.geolocated: this will be set to True. Since the .geolocate() method uses an API, it is important not to call the API repeatedly. The .geolocated attribute keeps track of whether the .geolocate() method has been called before. If it has been called before, and .geolocated is True, then calling .geolocate() again will not call the API again; rather, it will immediately return with whatever result was obtained the first time .geolocate() was called.
  • myaddress.geolocate_success: if .geolocate() was able to successfully call the API and get a result, this will be set to True; otherwise, this will be set to False.
  • myaddress.location.gathered: if .geolocate() was able to successfully call the API and get a result, this will be set to True.
  • myaddress.location.known: if .geolocate() was able to successfully call the API and get a result, this will be set to True.
  • myaddress.location.latitude: if .geolocate() was able to successfully call the API and get a result, this will be set to the latitude of the address.
  • myaddress.location.longitude: if .geolocate() was able to successfully call the API and get a result, this will be set to the latitude of the address.
  • myaddress.location.description: if .geolocate() was able to successfully call the API and get a result, this will be set to the value of myaddress.block().
  • myaddress.geolocate_response: if .geolocate() was able to successfully call the API and get a result, this will be set to the raw results returned from the Google Maps Geocoding API.
  • myaddress.one_line: if .geolocate() was able to successfully call the API and get a result, this will be set to the address as the geocoder would format it to be expressed on one line.
  • myaddress.norm: if .geolocate() was able to successfully call the API and get a result, this will be set to an Address object containing normalized names of the address components.
  • myaddress.norm_long: if .geolocate() was able to successfully call the API and get a result, this will be set to an Address object containing long-form normalized names of the address components. (E.g., “1234 Main Street” instead of “1234 Main St” and “California” instead of “CA.”)

In addition, the following attributes will be set if the attribute was not already set, and if .geolocate() was able to successfully determine the value by calling the API:

  • myaddress.street_number - the street number (e.g., 123).
  • myaddress.street - the street name (e.g., Main St).
  • myaddress.city - the city (known as (locality).
  • myaddress.county - the county (known as administrative_area_level_2).
  • myaddress.state - the state (known as administrative_area_level_1).
  • myaddress.zip - the Zip code (known as postal_code).
  • myaddress.country - the country (e.g., US).

In addition, the following attributes will be set to the “long” form of the values returned from the Google Maps Geocoding API, if applicable:

  • myaddress.administrative_area_level_1
  • myaddress.administrative_area_level_2
  • myaddress.administrative_area_level_3
  • myaddress.administrative_area_level_4
  • myaddress.administrative_area_level_5
  • myaddress.colloquial_area
  • myaddress.floor
  • myaddress.intersection
  • myaddress.locality
  • myaddress.neighborhood
  • myaddress.post_box
  • myaddress.postal_code
  • myaddress.postal_code_prefix
  • myaddress.postal_code_suffix
  • myaddress.postal_town
  • myaddress.premise
  • myaddress.room
  • myaddress.route
  • myaddress.sublocality
  • myaddress.sublocality_level_1
  • myaddress.sublocality_level_2
  • myaddress.sublocality_level_3
  • myaddress.sublocality_level_4
  • myaddress.sublocality_level_5
  • myaddress.subpremise

Here is an example that illustrates how the .geolocate() method works:

objects:
  - the_address: Address
---
question: |
  Enter an address
fields:
  - Address: the_address.address
  - Unit: the_address.unit
    required: False
  - City: the_address.city
  - State: the_address.state
  - Zip: the_address.zip
    required: False
---
mandatory: True
code: |
  the_address.geolocate()
---
mandatory: True
question: |
  Information about your address
subquestion: |
  The address you provided is:
    
  ${ the_address }

  As JSON, this is:

  ${ indent(json.dumps(the_address.as_serializable(), indent=2)) }

  % if the_address.geolocate_success:
  The normalized version is:

  ${ the_address.norm }

  As JSON, this is:

  ${ indent(json.dumps(the_address.norm.as_serializable(), indent=2)) }
  
  The long-form normalized
  version is:

  ${ the_address.norm_long }

  As JSON, this is:

  ${ indent(json.dumps(the_address.norm_long.as_serializable(), indent=2)) }

  The latitude and longitude are
  ${ the_address.location.latitude }
  and
  ${ the_address.location.longitude }.

  % if the_address.attr('neighborhood'):
  The address is located in the
  ${ the_address.neighborhood }
  neighborhood.
  % endif

  % if the_address.attr('county'):
  The address is located in
  ${ the_address.county }.
  % endif

  ${ map_of(the_address) }

  % else:
  The address could not be
  geolocated.
  % endif
geolocate

The .normalize() method uses the results of .geolocate() to standardize the formatting of the parts of the address. This will overwrite the attributes of the object. This method takes an optional keyword parameter long_format, which defaults to False. If this parameter is True, the address will be normalized using the long form of the normalization. (E.g., “California” instead of “CA.”)

objects:
  - the_address: Address
---
question: |
  Enter an address
fields:
  - Address: the_address.address
  - Unit: the_address.unit
    required: False
  - City: the_address.city
  - State: the_address.state
  - Zip: the_address.zip
    required: False
---
mandatory: True
code: |
  the_address.normalize()
---
mandatory: True
question: |
  The normalized address
subquestion: |
  ${ the_address }
normalize

Note that if you want to access a normalized version of the address, but you don’t want to overwrite the original attributes of the object, you can simply run .geolocate() and then, if it is successful, access the .norm attribute or the .norm_long attribute, both of which will be fully populated Address objects, with normalized attributes.

The .line_one() method returns the first line of the address, including the unit, if the unit is defined.

The .line_two() method returns the second line of the address, consisting of the city, state, and zip code.

The .on_one_line() method returns the address, consisting of the city, state, and zip code, as a single line of text.

It takes two optional keyword parameters:

  • include_unit - Default value is False. Set to True if you want the unit number to be included.
  • omit_default_country - Default value is True. Set to False if you want the country to be included. Normally, the country is included only if it is different from the default country.

Address autocomplete

If you have defined a google maps api key in the Configuration, you can use the Place Autocomplete feature of the Google Places API to help your users enter addresses. Address suggestions will be provided as the user begins to type. To use this feature, modify the street address (.address) field by setting address autocomplete to True. Then, when the user selects an address, the other components of the address will be filled in with the values obtained from Google. This will only work if the address components are part of the same Address object. For example, if the street address field is client.address.address, the other fields must be called client.address.city, client.address.state, etc. You do not need to include all of these attributes; any attributes not included in the front end of the page will be ignored.

For this feature to work, your google maps api key will need to be associated with an app for which the following APIs are enabled:

  • Google Places API Web Service
  • Google Maps JavaScript API

Here is an example that illustrates how the address autocomplete feature works:

question: |
  What is the address of the adverse
  party?
fields:
  - Address: defendant.address.address
    address autocomplete: True
  - City: defendant.address.city
  - State: defendant.address.state
    code: |
      states_list()
  - Zip: defendant.address.zip
    required: False
  - County: defendant.address.county
address-autocomplete

City

A City is a type of Address for which the attribute .city_only is set to True. Functions that display the address will only display the city, the state, and (if set) the zip code.

LatitudeLongitude

A LatitudeLongitude object represents the GPS coordinates of an address or location. LatitudeLongitude objects have the following attributes:

  • latitude: the latitude of the location.
  • longitude: the longitude of the location.
  • description: a textual description of the location.
  • known: whether the GPS location is known yet.
  • gathered: whether a determination of the GPS location has been attempted yet.

One use for the LatitudeLongitude object is for mapping the coordinates of an address. The Address object has a method .geolocate() for this purpose.

Another use for the LatitudeLongitude object is storing the GPS location of the user’s device. Many web browsers, particularly those on mobile devices, have a feature for determining the user’s GPS coordinates. Usually the browser asks the user to consent to the sharing of the location information. To support this feature, the LatitudeLongitude object offers the method .status().

The following example shows how to gather the user’s latitude and longitude from the web browser.

include:
  - basic-questions.yml
---
initial: True
code: |
  track_location = user.location.status()

Alternatively, if you do not want to include all of the questions and code blocks of the basic-questions.yml file in your interview, you can do:

modules:
  - docassemble.base.util
---
objects:
  - user: Individual
---
initial: True
code: |
  set_info(user=user, role='user_role')
  track_location = user.location.status()

If all goes well, the user’s latitude and longitude will be gathered and stored in user.location.latitude and user.location.longitude. You can control when this happens in the interview by controlling when track_location is set. For example, you may wish to prepare the user for this:

initial: True
code: |
  set_info(user=user, role='user_role')
  if user_ok_with_sharing_location:
    track_location = user.location.status()
---
question: |
  We would like to gather information about your current location
  from your mobile device.  Is that ok with you?
yesno: user_ok_with_sharing_location

track_location is a special variable that tells docassemble whether or not it should ask the browser for the user’s GPS coordinates the next time a question is posed to the user. If track_location is True, docassemble will ask the browser to provide the information, and if it receives it, it will make it available for retrieval through the user_lat_lon() function.

The .status() method looks to see if a latitude and longitude were provided by the browser, or in the alternative that an error message was provided, such as “the user refused to share the information,” or “this device cannot determine the user’s location.” If the latitude and longitude information is conveyed, .status() stores the information in user.location.latitude and user.location.longitude. The .status() method returns False in these situations, which means “we already asked for the latitude and longitude and got a response, so there is no longer any need for the browser to keep asking for it.” Otherwise, it returns True, which means “the browser has not yet been asked for the location information, so let’s ask it.”

Classes for currency

Value

A Value is a subclass of DAObject that is intended to represent a currency value that may or may not need to be asked for in an interview.

For example, suppose you want to have a variable that represents the value of the user’s real estate holdings. But before you ask the value of the user’s real estate holdings, you will want to ask if the user has real estate holdings at all.

A Value has two attributes, both of which are initially undefined:

  • .value: intended to be a number
  • .exists: a boolean value representing whether the value is applicable

The .exists attribute facilitates asking questions about values using two screens: first, ask whether the value exists at all, then ask for the value. For example:

modules:
  - docassemble.base.util
---
objects:
  - real_estate_holdings: Value
---
question: |
  Do you have real estate holdings?
yesno: real_estate_holdings.exists
---
question: |
  How much real estate do you own?
fields:
  - Value: real_estate_holdings.value
    datatype: currency
exists

The FinancialList object, explained below, represents a list of Values. When computing a total of the values (with .total()), it checks the .exists attributes of each Value to be defined. This causes questions to be asked about whether the Value is applicable to the user’s situation before the value itself is requested.

To access the value of a Value object, you can use the .amount() method. If the .exists attribute is False, it will return zero without asking for the .value.

Referring to a Value in a Mako template will show the .amount(). The value of .amount() is also returned when you pass a Value to the currency() function. For example:

question: |
  The value of your real estate holdings is
  ${ currency(real_estate_holdings) }.
  
  An identical way of writing this number is 
  ${ currency(real_estate_holdings.amount()) }.

PeriodicValue

A PeriodicValue is a Value that has an additional attribute, period, which is a number representing the number of times per year the value applies.

objects:
  - user_salary: PeriodicValue
---
question: |
  Do you make money from working?
yesno: user_salary.exists
---
question: |
  What is your salary?
fields:
  - Amount: user_salary.value
    datatype: currency
  - Period: user_salary.period
    default: 1
    choices:
      - Annually: 1
      - Monthly: 12
      - Per week: 54
---
mandatory: True
question: |
  % if user_salary.exists:
  You make ${ currency(user_salary) }
  per year.
  % else:
  Get a job!
  % endif
periodic-value

To access the value of a PeriodicValue object, you can use the .amount() method. If the .exists attribute is False, it will return zero without asking for the .value. By default, it returns the value for the period 1 (e.g., in the example above, period of 1 represents a year). That is, it will return the .value multiplied by the .period.

Referring to a PeriodicValue in a Mako template will show the .amount(). The value of .amount() is also returned when you pass a PeriodicValue to the currency() function.

Classes for lists of things

PartyList

This is a subclass of DAList.

It is indended to contain a list of Persons (or Individuals, which are a type of Person) who are parties to a case.

ChildList

This is a subclass of DAList.

It is indended to contain a list of Individuals who are children.

FinancialList

The FinancialList is intended to collect a set of financial items, such as an individual’s assets. It is a DADict object with an object_type of Value. The exists attribute of each Value item is set by default to True.

The FinancialList has three methods:

  • .total(): tallies up the total value of all Values in the list for which the exists attribute is True. A reference to .total() will cause docassemble to ask the questions necessary to gather the full list of items.
  • .existing_items(): returns a list of types of amounts that exist within the financial list.

In the context of a Mako template, a FinancialList returns the result of .total().

Asset

This is a subclass of FinancialList that is intended to be used to track assets.

Here is some example code that triggers questions that ask about asset items.

objects:
  - decedent: Individual
  - estate: Thing
  - estate.asset: Asset
---
question: |
  Who is the decedent?
fields:
  - First Name: decedent.name.first
  - Last Name: decedent.name.last
---
code: |
  estate.name.text = "estate of " + decedent.name.full()
---
question: |
  Does the ${ estate } have
  any assets?
yesno: estate.asset.there_are_any
---
question: |
  What type of asset does
  the ${ estate } have?
fields:
  - Type of asset: estate.asset.new_item_name
    hint: |
      e.g., real estate, 401(k), jewelry
---
question: |
  What is the value of ${ i }
  in the ${ estate }?
fields:
  - Value: estate.asset[i].value
    datatype: currency
---
question: |
  Is there another asset in
  the ${ estate }?
yesno: estate.asset.there_is_another
---
mandatory: True
question: |
  Summary of the ${ estate }.
subquestion: |
  The total value of
  ${ decedent.possessive('estate') }
  is
  ${ currency(estate.asset.total()) }.
assets

PeriodicFinancialList

This is a subclass of FinancialList intended to collect a set of financial items that have a periodic nature, such as an individual’s income. Instead of each item being a Value, each item is a PeriodicValue.

The PeriodicFinancialList has the following method:

  • .total(): tallies up the total annual value of all PeriodicValues in the list for which the exists attribute is True.

If you have a PeriodicFinancialList called income, you can have a single question that asks for the item name for a new item, and also the value and period of the new item. Just write a question that sets these three attributes:

  • income.new_item_name
  • income.new_item_value
  • income.new_item_period

In the context of a Mako template, a PeriodicFinancialList returns .total().

Income

This is a subclass of PeriodicFinancialList with no special properties except the name.

Expense

Expense is a PeriodicFinancialList representing a person’s expenses. It has no special properties except the name.

OfficeList

An OfficeList object is a type of DAList, the items of which are expected to be Address objects. It is used in Organization objects.

Classes for special purposes

RoleChangeTracker

The RoleChangeTracker class is provided to facilitate multi-user interviews with docassemble’s roles system. It facilitates sending e-mails to the participants to let them know when the interview needs their attention. It keeps track of when these e-mails have been sent to make sure that duplicative e-mails are not sent.

It has one method:

  • role_change.send_email() (not to be confused with the send_email() function)

Here is an example that demonstrates its use:

modules:
  - docassemble.base.util
---
objects:
  - client: Individual
  - advocate: Individual
  - role_change: RoleChangeTracker
---
default role: client
code: |
  if user_logged_in() and \
      advocate.attribute_defined('email') and \
      advocate.email == user_info().email:
    user = advocate
    role = 'advocate'
  else:
    user = client
    role = 'client'
  set_info(user=user, role=role)
---
event: role_event
question: You are done for now.
subquestion: |
  % if 'advocate' in role_needed:
  An advocate needs to review your answers before you can proceed.

  Please remember the following link and come back to it when you
  receive notice to do so:

  [${ interview_url() }](${ interview_url() })
  % else:
  Thanks, the client needs to resume the interview now.
  % endif

  % if role_change.send_email(role_needed, advocate={'to': advocate, 'email': role_event_email_to_advocate}, client={'to': client, 'email': role_event_email_to_client}):
  An e-mail has been sent.
  % endif
decoration: exit
buttons:
  - Exit: leave
---
template: role_event_email_to_advocate
subject: |
  Client interview waiting for your attention: ${ client }
content: |
  A client, ${ client }, has partly finished an interview.

  Please go to [the interview](${ interview_url() }) as soon as
  possible to review the client's answers.
---
template: role_event_email_to_client
subject: |
  Your interview answers have been reviewed
content: |
  An advocate has finished reviewing your answers.

  Please go to [${ interview_url() }](${ interview_url() })
  to resume the interview.

The send_email() method’s first argument is the special variable role_needed, a Python list that docassemble defines internally whenever there is a mismatch between the current user’s role and the role required by a question that needs to be asked.

The remaining arguments to send_email() are keyword arguments, where each keyword is the name of a possible role. Each keyword argument must be a Python dictionary containing the following keys:

  • to: this needs to be a Person (or a subclass, like Individual). The person’s email attribute is expected to be defined.
  • email: this needs to a DATemplate containing the subject and body of the e-mail that will be sent.

Special object method using()

If you wanted to initialize the variable possession as a DAList of Things, you could write

objects:
  - possession: DAList
---
mandatory: True
code: |
  possession.object_type = Thing

The DAObject class has a special object method, using(), which can be used to accomplish the same thing in a more compact way, so that you could instead write:

objects:
  - possession: DAList.using(object_type=Thing)

You can use using() when indicating an object_type:

objects:
  - client: Individual
  - possession: DAList.using(object_type=Thing.using(owner=client))

The result of this will be that possession is a DAList of Things, and each item in the list will be initialized so that the owner attribute is set to client by default.

The result of .using() can be used in a number of contexts, including objects initial blocks, as a parameter in the methods initializeAttribute(), reInitializeAttribute(), appendObject(), and gather(), or as the object_type attribute of a DAList or DADict.

How docassemble objects are different

For most purposes, docassemble objects behave just like Python objects. However, they have special properties that facilitate the automatic asking of questions. You may need to be mindful of these special properties if you do anything fancy in your code.

In contrast to Python objects in general, docassemble objects are aware of their first-given names. All docassemble objects have an .instanceName attribute. So if you do:

objects:
  - user: Individual

then user.instanceName will be 'user', and user.name.instanceName will be 'user.name'.

You can also initialize user in standard Python fashion:

code: |
  user = Individual()

In this circumstance, docassemble uses some magic to set .instanceName to user. However, the magic has its limits. For example, the following does not work:

code: |
  (user, advocate) = (Individual(), Individual())

If you ever get an error message in docassemble referring to variables with a name like qjAMyvGQYnyK, and you are sure you did not create such a variable, then you have an object that was unable to determine its given name.

If you want to initialize objects using expressions more complicated than variable_name = ObjectName(), you can – you just need to include the variable name as an argument to the object name. For example:

code: |
  (user, advocate) = (Individual('user'), Individual('advocate'))

Attribute initialization does not have this limitation.

Always keep in mind that objects are given .instanceName attributes as early as possible, and once an .instanceName is assigned, it will not be overwritten unless you explicitly overwrite it. For example, if you do:

code: |
  user.name = IndividualName()

then user.name.instanceName will return 'user.name', as you would expect. But if you do:

code: |
  cool_name = IndividualName()
  cool_name.first = 'Groovy'
  cool_name.last = 'Jones'
---
code: |
  user.name = cool_name()

then user.name.instanceName will be 'cool_name', not 'user.name'.

You can manually correct this:

code: |
  user.name = cool_name()
  user.name.instanceName = 'user.name'

The .instanceName is not simply an internal attribute; it is used by the .object_possessive() method to refer to the object in human-readable format.

Writing your own classes

If you know how to write your own Python code, it is pretty easy to write your own classes.

For example, you could create your own package for interviews related to cooking.

You would start by using the package system to create a docassemble package called cooking, the full name of which would be docassemble.cooking (interview packages are subpackages of the docassemble namespace package).

You would create a module file within this package called objects.py. If you are using the Playground, you would create this file in the Modules folder of the Playground. Otherwise, you would create this file in the docassemble/cooking directory in your package. You would set the contents of objects.py to the following:

from docassemble.base.core import DAObject

class Recipe(DAObject):
    def summary(self):
        return "#### Ingredients\n\n" + self.ingredients + "\n\n#### Instructions\n\n" + self.instructions

Your class Recipe needs to “inherit” from the basic docassemble object called DAObject. If you did not do this, docassemble would not be able to ask questions to define attributes of Recipe objects.

The purpose of the summary() method is to summarize the contents of the recipe. It makes use of the attributes ingredients and instructions.

If you are not familiar with Python, \n inside quotation marks indicates a line break and + in the context of text indicates that the text should be strung together. In Markdown, #### at the start of a line indicates that the line is a section name.

You can use your class in an interview like this:

modules:
  - .objects
---
objects:
  - submission: Recipe
---
mandatory: True
question: |
  Recipe for ${ submission.name }
subquestion: |
  ${ submission.summary() }
---
generic object: Recipe
question: |
  What do you want to cook?
fields:
  - Food: x.name
---
generic object: Recipe
question: |
  What are the ingredients in
  ${ x.name }?
fields:
  - no label: x.ingredients
    datatype: area
---
generic object: Recipe
question: |
  What are the instructions
  for cooking ${ x.name }?
fields:
  - no label: x.instructions
    datatype: area
testcooking

Note that the modules block refers to .objects, which is a relative module name. The . at the beginning means “in the current package.” You could also have written docassemble.cooking.objects. The relative module name works so long as the interview file is in the same package as the module.

By the way, there is way to write the summary() method that is more friendly to other interview authors:

from docassemble.base.core import DAObject
from docassemble.base.functions import word

class Recipe(DAObject):
    def summary(self):
        return "#### " + word('Ingredients') + "\n\n" + self.ingredients + "\n\n#### " + word('Instructions') + "\n\n" + self.instructions

If you use the word() function in this way, interview authors will be able to translate the “cooking” interview from English to another language without having to edit your code. All they would need to do is include the words Ingredients and Instructions in a translation YAML file referenced in a words directive in the docassemble configuration.

Initializing object attributes

In the example above, all the attributes of the Recipe object were plain text values. What if you want attributes of your objects to be objects themselves?

Suppose you want the ingredients attribute to be a DAList.

There are several ways that ingredients can be initialized. In the interview itself, you can do:

modules:
  - docassemble.cooking
  - docassemble.base.util
---
objects:
  - dinner: Recipe
  - dinner.ingredients: DAList

Or, you could use sets in combination with initializeAttribute():

modules:
  - docassemble.cooking
  - docassemble.base.util
---
objects:
  - dinner: Recipe
---
generic object: Recipe
sets: x.ingredients
code: |
  x.initializeAttribute('ingredients', DAList)

However, it is often cleaner to put the object initialization into the class definition itself:

class Recipe(DAObject):
    def init(self, *pargs, **kwargs):
        self.initializeAttribute('ingredients', DAList)
        super(Recipe, self).init(*pargs, **kwargs)

Then, you would only need to write this in your interview file:

objects:
  - dinner: Recipe

The init() function is a special function that is called on all DAObject objects at the time they are initialized. This is not to be confused with the __init__() function, which is built in to Python; you should use init(), not __init__().

When you write your own init() function for a class, you should (but are not required to) include the super(Recipe, self).init(*pargs, **kwargs) line. This will ensure that Recipe objects are initialized properly. For example, if you wrote:

dinner.initializeAttribute('recipe', Recipe, oven_temperature=300)

then dinner.recipe would be a Recipe object and dinner.recipe.oven_temperature would be 300. However, if you included an init() function and failed to include super(Recipe, self).init(*pargs, **kwargs), then the oven_temperature variable would not be set. Therefore, it is a good practice to always write your init() methods in this way.

Before you use objects and inheritance, you should buy a Python book and learn how Python handles object orientation. Object oriented programming is an advanced topic and docassemble documentation is not a substitute for Python documentation.

Using global variables in your classes

Normally in Python you can use global variables to keep track of information that your methods need to know but that is not passed in arguments to the methods. For example, if you wanted to keep track of whether to use Celsius or Fahrenheit when talking about temperatures, you might be tempted to write:

from docassemble.base.core import DAObject

temperature_type = 'Celsius'

class Recipe(DAObject):
    def summary(self):
        return "#### Ingredients\n\n" + self.ingredients + "\n\n#### Instructions\n\n" + self.instructions
    def get_oven_temperature(self):
        if temperature_type == 'Celsius':
            return str(self.oven_temperature) + ' °C'
        elif temperature_type == 'Fahrenheit':
            return str(self.oven_temperature) + ' °F'
        elif temperature_type == 'Kelvin': 
            return str(self.oven_temperature) + ' K'

(The str() function is a Python function that converts something to text. Here, it is necessary because self.oven_temperature may be a number, and Python will complain if you ask it to “add” text to a number.)

Then to change the temperature_type from an interview, you might write:

modules:
  - docassemble.cooking.objects
---
initial: True
code: |
  if user_is_scientist:
    temperature_type = 'Kelvin'
  elif user_country in ['United States', 'Great Britain']:
    temperature_type = 'Fahrenheit'

This would be effective at changing the temperature_type variable because the modules block loads all the names from docassemble.cooking.objects into the variable store of the interview, including temperature_type.

However, this is not thread-safe and it will not work correctly 100% of the time. If your server is under heavy load, users might randomly be advised to turn their ovens to 350 degrees Celsius, which would scorch the food. This is because the variable temperature_type exists at the level of the web server process, and the process might be supporting several users simultaneously (in different “threads” of the process). Between the time one user sets temperature_type to Fahrenheit and tries to use it, another user inside the same process might set temperature_type to Celsius.

Therefore, it is important that you do not use global variables when you write your own classes. The simplest way to get around this problem is to use the set_info() and get_info() functions from docassemble.base.util:

from docassemble.base.core import DAObject
from docassemble.base.util import get_info

class Recipe(DAObject):
    def summary(self):
        return "#### Ingredients\n\n" + self.ingredients + "\n\n#### Instructions\n\n" + self.instructions
    def get_oven_temperature(self):
        if get_info('temperature_type') == 'Celsius':
            return str(self.oven_temperature) + ' °C'
        elif get_info('temperature_type') == 'Fahrenheit':
            return str(self.oven_temperature) + ' °F'
        elif get_info('temperature_type') == 'Kelvin': 
            return str(self.oven_temperature) + ' K'

Then from your interview you can include docassemble.base.util as one of the modules and then run set_info() in initial code:

modules:
  - docassemble.base.util
  - docassemble.cooking.objects
---
initial: True
code: |
  if user_is_scientist:
    set_info(temperature_type='Kelvin')
  elif user_country in ['United States', 'Great Britain']:
    set_info(temperature_type='Fahrenheit')
  else:
    set_info(temperature_type='Celsius')

The values set by set_info() are forgotten after the user’s screen is prepared. Therefore, it is necessary to run set_info() in an initial code block so that values like temperature_type are put in place before they are needed.

If you are an advanced programmer, you can do what docassemble.base.util does and use Python’s threading module to store global variables.

from docassemble.base.core import DAObject
import threading

__all__ = ['set_temperature_type', 'get_temperature_type', 'Recipe']

this_thread = threading.local

def set_temperature_type(type):
    this_thread.temperature_type = type

def get_temperature_type():
    return this_thread.temperature_type

class Recipe(DAObject):
    def summary(self):
        return "#### Ingredients\n\n" + self.ingredients + "\n\n#### Instructions\n\n" + self.instructions
    def get_oven_temperature(self):
        if this_thread.temperature_type == 'Celsius':
            return str(self.oven_temperature) + ' °C'
        elif this_thread.temperature_type == 'Fahrenheit':
            return str(self.oven_temperature) + ' °F'
        elif this_thread.temperature_type == 'Kelvin': 
            return str(self.oven_temperature) + ' K'

We added an __all__ statement so that interviews can a module block including docassemble.cooking.objects does not clutter the variable store with extraneous names like threading. We also added functions for setting and retrieving the value of the “temperature type.”

The temperature type is now an attribute of the object this_thread, which is an instance of threading.local. This attribute needs to be set in initial code that will run every time a screen refreshes.

Now in your interview you can do:

modules:
  - docassemble.cooking.objects
---
initial: True
code: |
  if user_is_scientist:
    set_temperature_type('Kelvin')
  elif user_country in ['United States', 'Great Britain']:
    set_temperature_type('Fahrenheit')
  else:
    set_temperature_type('Celsius')

Note that you do not need to worry about whether your global variables are thread-safe if they do not change from interview to interview.

For example, if you only wanted to allow people to change the temperature type from the docassemble configuration, you could do the following in your Python module:

from docassemble.base.core import DAObject

from docassemble.webapp.config import daconfig
temperature_type = daconfig.get('temperature type', 'Celsius')

Then your interviews would not have to do anything with temperature_type.

Also, you could avoid the complication of global variables entirely if you are willing to pass the temperature type as an argument to get_oven_temperature:

from docassemble.base.core import DAObject

class Recipe(DAObject):
    def get_oven_temperature(self, type):
        if type == 'Celsius':
            return str(self.oven_temperature) + ' °C'
        elif type == 'Fahrenheit':
            return str(self.oven_temperature) + ' °F'
        elif type == 'Kelvin': 
            return str(self.oven_temperature) + ' K'

Then you could have this in your interview:

question: |
  What kind of temperature system do you use?
choices:
  - Celsius
  - Fahrenheit
  - Kelvin
field: temperature_type

and then in your question text you could write:

Set your oven to ${ apple_pie.get_oven_temperature(temperature_type) }
and let it warm up.

Extending existing classes

If you want to add a method to an existing docassemble class, such as Individual, you do not need to reinvent the wheel or copy and paste code from anywhere. Just take advantage of inheritance.

For example, if your package is docassemble.missouri_family_law, you could create/edit the file docassemble/missouri_family_law/objects.py and add the following:

from docassemble.base.util import Individual

class Attorney(Individual):
    def can_practice_in(self, state):
        if state in self.bar_admissions and self.bar_admissions[state] is True:
            return True
        return False

Here you are defining the class Attorney as a subclass of Individual. An object that is an instance of the Attorney class will also be an instance of the Individual class. The Attorney class is said to “inherit” from the Individual class. All of the methods that can be used on an Individual can be used on an Attorney.

This allows you to write an interview like the following:

modules:
  - docassemble.demo.attorney
---
imports:
  - us
---
objects:
  - lawyer: Attorney
---
mandatory: True
question: |
  % if lawyer.can_practice_in('MA'):
  ${ lawyer } can practice in Massachusetts.
  % else:
  Sorry, ${ lawyer } cannot practice in Massachusetts.
  % endif
---
generic object: Attorney
question: |
  In what state(s) ${ x.is_are_you() } admitted to practice?
fields:
  - no label: x.bar_admissions
    datatype: checkboxes
    code: |
      us.states.mapping('abbr', 'name')
---
generic object: Attorney
question: |
  What is the attorney's name?
fields:
  - First Name: x.name.first
  - Last Name: x.name.last
attorney

Note that the lawyer object works just like an Individual object. The is_are_you() method, which is defined in docassemble.base.util, works on the Attorney object, despite the fact that the interview does not explicitly refer to docassemble.base.util anywhere. (The module docassemble.missouri_family_law.objects imports docassemble.base.util.)

Note that the can_practice_in() method is only available for Attorney objects. If you added the following to the above interview:

objects:
  - user: Individual
---
mandatory: True
question: |
  % if user.can_practice_in('MA'):
  You can take this case yourself.
  % else:
  You will need to hire a lawyer to take the case.
  % endif

then you would get an error because can_practice_in() is not a valid method for user, which is only an instance of the Individual class and not an instance of the Attorney class.

Special date/time class DADateTime

When you set a variable with datatype: date, or use one of the date functions that returns a date, the variable is a special object of the class DADateTime. This object is special to docassemble, but it is not a DAObject. You cannot create these with an objects block. (If you want to create one, use as_datetime().)

The DADateTime object is a subclass of datetime.datetime, which is a standard Python class for working with dates and times. This means that anything that you can do with a datetime.datetime object can also be done with a DADateTime object.

The DADateTime object also has some additional functionality that the traditional datetime.datetime object does not have, such as the attributes dow for day of week and week for the week of the year.

If birthday is defined by as_datetime('4/1/2018'), then:

  • birthday.day is 1
  • birthday.month is 4
  • birthday.year is 2018
  • birthday.week is 13
  • birthday.dow is 7 (Sunday)
  • birthday.hour is 0
  • birthday.minute is 0
  • birthday.second is 0
  • birthday.microsecond is 0

The DADateTime object also has methods .plus() and .minus() that allow you to add or subtract periods of time from a date.

  • birthday.plus(weeks=3) returns a DADateTime object representing April 29, 2018.
  • birthday.plus(months=1) returns a DADateTime object representing May 1, 2018.
  • birthday.minus(years=2) returns a DADateTime object representing April 1, 2020.

The available keyword arguments to .plus() and .minus() are:

  • years
  • months
  • days
  • weeks
  • hours
  • minutes
  • seconds
  • microseconds

The .plus() and .minus() methods use dateutil.relativedelta.relativedelta to calculate dates and times. The date_interval() function can be used to do similar calculations. For example, birthday.plus(weeks=1) is equivalent to doing birthday + date_interval(weeks=1).

The DADateTime object also has methods for formatting dates and times.

  • birthday.format() is 'April 1, 2018'.
  • birthday.format_date() is 'April 1, 2018' (identical to .format())
  • birthday.format_date('MMM') is 'Apr'
  • birthday.format_time() is '12:00 AM'
  • birthday.format_time('h:mm a z') is '12:00 AM EST', or whatever the current time zone is.
  • current_datetime().format_time('h:mm') returns the current time, formatted like '12:00'.
  • current_datetime().format_datetime() returns the current time, formatted like 'January 1, 2018 at 12:00:00 AM EST'.

These functions have the same effect as the format_date(), format_time(), and format_datetime() functions. In fact, birthday.format_date('long') simply calls format_date(birthday, format='long'). See the documentation for the date functions for details.

When a DADateTime is converted to text, for example when it is included in a Mako template with ${ birthday }, the text conversion is done using format_date().

The .replace() method returns a new DADateTime object that is the same as the original object, excep with edited components. For example, birthdate.replace(year=2018) will return the date of a person’s birthday in 2018. The available parameters are year, month, day, hour (0 to 23), minute, second, and microsecond. See datetime.datetime.replace().

A method that is similar to .replace() is .replace_time(), which returns a DADateTime object with all of the time-related values set to those of a given datetime.time object. If you ask the user a question and you use a field with datatype: time, the resulting variable is a datetime.time object. You can combine datatype: date and datatype: time objects using .replace_time():

question: |
  When is your appointment?
fields:
  - Date: appt_date
    datatype: date
  - Time: appt_time
    datatype: time
---
code: |
  appt_datetime = appt_date.replace_time(appt_time)
date-and-time-fields

If you only want to use the time portion of a DADateTime, use the .time() method, which returns a datetime.time object containing only the time-related information of the original DADateTime object. See datetime.datetime.time().