jhpyle / docassemble

A free, open-source expert system for guided interviews and document assembly, based on Python, YAML, and Markdown.
https://docassemble.org
MIT License
796 stars 258 forks source link

Table delete button gives error when table list is contained within dictionary #148

Closed lordenaar closed 5 years ago

lordenaar commented 5 years ago

When a DAList is contained within a dictionary, the standard list gathering does not work. We tried a range of options to get a similar concept to work, but it seems like DA can potentially not 'find' the name of a DAList if it is contained within a dictionary, DADict or python dictionary regardless (the error is often, 'instanceName is not defined', despite the instance name existing in the developer variables and values view).

This example below uses the standard DA addition and removal methods, and crashes on run:

code: |
  example_dict = {}
  example_dict['exlist'] = DAList()
---
mandatory: true
question: Items Table
fields:
  - note: |
      ${ items_table }
      ${ example_dict['exlist'].add_action() }
---
table: items_table
rows: example_dict['exlist']
columns:
  - item: |
      row_item
delete buttons: True
---
question: Add item
fields:
  - Description: example_dict['exlist'][i]

continue button field: item_questions
---
question: |
  Are there any items that you would like
  to add to the list?
yesno: example_dict['exlist'].there_are_any
---
question: |
  So far, the items include ${ example_dict['exlist'] }.
  Are there any others?
yesno: example_dict['exlist'].there_is_another

We can instead add a custom addition function, which works, but the DA table delete button will then not work: (Throwing the name error as discussed above, despite the name being findable inside the variables and values log)

code: |
  dictionary = DADict()
  dictionary['items'] = DAList()
  dictionary['items'].there_are_any = False
---
mandatory: true
question: Items Table
fields:
  - note: |
      ${ items_table }

      Custom button:
      ${ action_button_html(url_action('add_item'), icon='plus', color='success', label='Add item') }

      Standard button:
      ${ dictionary['items'].add_action() }
---
table: items_table
rows: dictionary['items']
columns:
  - item: |
      row_item
delete buttons: True
---
event: add_item
code: |
  i = len(dictionary['items'])
  dictionary['items'].append('')
  force_ask('item_questions')
---
question: Add item
fields:
  - Description: dictionary['items'][i]

continue button field: item_questions

To handle this situation, should we just create custom add and delete buttons ourselves?

jhpyle commented 5 years ago

Here is an example of an interview that shows a table of a list, where the list is embedded in a dictionary.

objects:
  - food: DADict.using(there_are_any=True)
  - food[i]: DAList.using(there_are_any=True)
---
question: |
  Give me a type of food.
fields:
  Type of food: food.new_item_name
---
table: food[i].table
rows: food[i]
columns:
  - "#": row_index
  - "Type of ${ i }": row_item
delete buttons: True
---
question: |
  Give me the ${ ordinal(j) } ${ i }.
fields:
  - "Name of a ${ i }": food[i][j]
---
question: |
  Are there any more ${ i } you would like to mention?
yesno: food[i].there_is_another
---
question: |
  Are there any more food types you would like to mention?
yesno: food.there_is_another
---
question: |
  What type of food would you like to review?
field: food_type
choices:
  code: |
    food.keys()
---
mandatory: True
question: |
  Summary of ${ food_type }
subquestion: |
  ${ food[food_type].table }

  ${ food[food_type].add_action() }

i is a special variable, so you cannot set it yourself. You will run into problems if you try to set it yourself. It is set and reset internally by the group-gathering system.

When a DAObject is initialized, it needs to be initialized in such a way that it is aware of its own variable name. If it does not know its name, or it knows the wrong name, systems like list-gathering and question-asking when there is an undefined name will not work. Python is a very good general-purpose programming language but it does not have a feature for variables being self-aware of their own names. Thus I had to hack it a bit and use an attribute called instanceName to store the variable name, and then trap all of the AttributeError, NameError, and IndexError exceptions

In your first example, you had:

example_dict['exlist'] = DAList()

There is some very ugly code that is run when Python does DAList() that attempts to figure out what is on the left side of the equals sign. However, this code is not reliable and it can get confused in certain contexts and by certain variable types. To be safe you should write:

example_dict['exlist'] = DAList("example_dict['exlist']")

(The first parameter to every object initializer is the instanceName.)

It may work in some contexts to nest a DAList inside of an ordinary Python dict, if you can set the instanceName appropriately. But in other contexts you might run into problems because docassemble's dependency satisfaction system and group gathering systems depend on objects being DAObjects, which have special properties.

I always initialize objects using the objects block, which takes care of setting the instanceName.