docassemble.AssemblyLine 4.6.2__tar.gz → 4.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {docassemble_assemblyline-4.6.2/docassemble.AssemblyLine.egg-info → docassemble_assemblyline-4.7.0}/PKG-INFO +2 -2
  2. docassemble_assemblyline-4.7.0/docassemble/AssemblyLine/__init__.py +1 -0
  3. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/al_document.py +40 -36
  4. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/al_general.py +33 -31
  5. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_saved_sessions.yml +1 -1
  6. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/ql_baseline.yml +1 -1
  7. docassemble_assemblyline-4.7.0/docassemble/AssemblyLine/data/questions/test_saving_snapshots.yml +17 -0
  8. docassemble_assemblyline-4.7.0/docassemble/AssemblyLine/data/sources/test_snapshots.feature +10 -0
  9. docassemble_assemblyline-4.7.0/docassemble/AssemblyLine/data/templates/general_cover_sheet.docx +0 -0
  10. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/sessions.py +82 -54
  11. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/test_al_document.py +30 -1
  12. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/test_al_general.py +6 -4
  13. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0/docassemble.AssemblyLine.egg-info}/PKG-INFO +2 -2
  14. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble.AssemblyLine.egg-info/SOURCES.txt +2 -0
  15. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble.AssemblyLine.egg-info/requires.txt +1 -1
  16. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/pyproject.toml +4 -2
  17. docassemble_assemblyline-4.6.2/docassemble/AssemblyLine/__init__.py +0 -1
  18. docassemble_assemblyline-4.6.2/docassemble/AssemblyLine/data/templates/general_cover_sheet.docx +0 -0
  19. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/LICENSE +0 -0
  20. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/MANIFEST.in +0 -0
  21. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/README.md +0 -0
  22. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/al_courts.py +0 -0
  23. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/custom_jinja_filters.py +0 -0
  24. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_code.yml +0 -0
  25. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_document.yml +0 -0
  26. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_language.yml +0 -0
  27. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_package.yml +0 -0
  28. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_package_unstyled.yml +0 -0
  29. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_question_test.yml +0 -0
  30. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_reminders.yml +0 -0
  31. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_saved_sessions_store.yml +0 -0
  32. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_settings.yml +0 -0
  33. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_terms_of_use.yml +0 -0
  34. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/al_visual.yml +0 -0
  35. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/assembly_line.yml +0 -0
  36. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/assembly_line_unstyled.yml +0 -0
  37. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/demo_search_sessions.yml +0 -0
  38. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/feedback.yml +0 -0
  39. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/interview_list.yml +0 -0
  40. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/ql_catchall.yml +0 -0
  41. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/ql_namechange.yml +0 -0
  42. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/reserved_words.yml +0 -0
  43. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_al_table_document.yml +0 -0
  44. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_aladdendum.yml +0 -0
  45. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_alcourts.yml +0 -0
  46. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_aldocument.yml +0 -0
  47. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_aldocument_background_assembly.yml +0 -0
  48. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_alexhibit.yml +0 -0
  49. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_alexhibit_maxsize.yml +0 -0
  50. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_bundle_parity.yml +0 -0
  51. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_catchall.yml +0 -0
  52. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_familiar_disambiguation.yml +0 -0
  53. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_if_final.yml +0 -0
  54. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_preferred_formats.yml +0 -0
  55. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_preferred_name.yml +0 -0
  56. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_question_library.yml +0 -0
  57. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/questions/test_signature.yml +0 -0
  58. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/README.md +0 -0
  59. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/ar-words.yml +0 -0
  60. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/es-words.yml +0 -0
  61. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/fr-words.yml +0 -0
  62. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/languages.yml +0 -0
  63. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/ne-words.yml +0 -0
  64. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/test_aldocument.feature +0 -0
  65. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_docx_1.docx +0 -0
  66. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_jpg_1.jpg +0 -0
  67. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_pdf_1.pdf +0 -0
  68. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_png_1.png +0 -0
  69. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/test_question_library.feature +0 -0
  70. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/translation_es.xlsx +0 -0
  71. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/translation_ht.xlsx +0 -0
  72. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/sources/translation_pt.xlsx +0 -0
  73. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/README.md +0 -0
  74. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/al_audio.css +0 -0
  75. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/al_audio.js +0 -0
  76. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/aldocument.css +0 -0
  77. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/aldocument.js +0 -0
  78. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/interview_list.css +0 -0
  79. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/limit_upload_size.js +0 -0
  80. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/lt1-pullout-10-getting-organized.pdf +0 -0
  81. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/ma_logo.png +0 -0
  82. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/placeholder.png +0 -0
  83. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/seal.jpg +0 -0
  84. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/static/styles.css +0 -0
  85. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/templates/README.md +0 -0
  86. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/templates/al_basic_addendum.docx +0 -0
  87. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/templates/exhibit_cover_sheet.docx +0 -0
  88. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/templates/exhibit_table_of_contents.docx +0 -0
  89. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/templates/sample_word_template.docx +0 -0
  90. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/templates/test_aldocument_docx_1.docx +0 -0
  91. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/templates/test_aldocument_pdf_1.pdf +0 -0
  92. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/templates/test_catchall.docx +0 -0
  93. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/data/templates/test_if_final.docx +0 -0
  94. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/language.py +0 -0
  95. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/py.typed +0 -0
  96. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/sign.py +0 -0
  97. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/test_all_imports.py +0 -0
  98. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble/AssemblyLine/test_language.py +0 -0
  99. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble.AssemblyLine.egg-info/dependency_links.txt +0 -0
  100. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/docassemble.AssemblyLine.egg-info/top_level.txt +0 -0
  101. {docassemble_assemblyline-4.6.2 → docassemble_assemblyline-4.7.0}/setup.cfg +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassemble.AssemblyLine
3
- Version: 4.6.2
3
+ Version: 4.7.0
4
4
  Summary: Quickly go from a paper court form to a runnable, guided, step-by-step web application powered by Docassemble. Swap out branding and pre-built questions to meet your needs.
5
5
  Author-email: Suffolk Legal Innovation and Technology Lab <litlab@suffolk.edu>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://courtformsonline.org
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
- Requires-Dist: docassemble.ALToolbox>=0.16.0
10
+ Requires-Dist: docassemble.ALToolbox>=0.18.0
11
11
  Requires-Dist: docassemble.GitHubFeedbackForm>=0.4.1.1
12
12
  Dynamic: license-file
13
13
 
@@ -0,0 +1 @@
1
+ __version__ = '4.7.0'
@@ -1,6 +1,8 @@
1
- import re
2
- import os
1
+ import json
3
2
  import mimetypes
3
+ import os
4
+ import re
5
+ from html import escape
4
6
  from typing import Any, Dict, List, Literal, Union, Callable, Optional
5
7
  from docassemble.base.util import (
6
8
  Address,
@@ -175,6 +177,14 @@ def html_safe_str(the_string: str) -> str:
175
177
  return re.sub(r"[^A-Za-z0-9]+", "_", the_string)
176
178
 
177
179
 
180
+ def _javascript_href(function_name: str, *args: Any) -> str:
181
+ """Build a JavaScript URL that is safe to use in an HTML href attribute."""
182
+ return escape(
183
+ f"javascript:{function_name}({','.join(json.dumps(arg) for arg in args)})",
184
+ quote=True,
185
+ )
186
+
187
+
178
188
  def table_row(title: str, button_htmls: List[str] = []) -> str:
179
189
  """
180
190
  Generate an HTML row string for an AL document-styled table.
@@ -2408,10 +2418,11 @@ class ALDocumentBundle(DAList):
2408
2418
  al_email_input_id = "_ignore_al_doc_email_" + name
2409
2419
  al_send_button_id = "al_send_email_button_" + name
2410
2420
 
2411
- javascript_string = (
2412
- f"javascript:aldocument_send_action("
2413
- f"'{self.attr_name('send_email_action_event')}',"
2414
- f"'{al_wants_editable_input_id}','{al_email_input_id}')"
2421
+ javascript_string = _javascript_href(
2422
+ "aldocument_send_action",
2423
+ str(self.attr_name("send_email_action_event")),
2424
+ al_wants_editable_input_id,
2425
+ al_email_input_id,
2415
2426
  )
2416
2427
 
2417
2428
  # Label "email" and input field for the 1st column of the table row
@@ -2474,23 +2485,23 @@ class ALDocumentBundle(DAList):
2474
2485
  name = html_safe_str(self.instanceName) + random_suffix()
2475
2486
  al_send_button_id = "al_send_email_to_button_" + name
2476
2487
 
2488
+ formats_value: Optional[Union[str, List[str]]]
2477
2489
  if isinstance(preferred_formats, (list, tuple)):
2478
- formats_js = "[" + ",".join(f"'{fmt}'" for fmt in preferred_formats) + "]"
2490
+ formats_value = [str(fmt) for fmt in preferred_formats]
2479
2491
  elif preferred_formats:
2480
- formats_js = f"'{preferred_formats}'"
2492
+ formats_value = str(preferred_formats)
2481
2493
  else:
2482
- formats_js = "null"
2483
-
2484
- javascript_string = (
2485
- f"javascript:aldocument_send_to_action("
2486
- f"'{self.attr_name('send_email_to_action_event')}',"
2487
- f"'{editable}',"
2488
- f"'{email}',"
2489
- f"'{al_send_button_id}',"
2490
- f"'{template_name}',"
2491
- f"'{key}',"
2492
- f"{formats_js}"
2493
- f")"
2494
+ formats_value = None
2495
+
2496
+ javascript_string = _javascript_href(
2497
+ "aldocument_send_to_action",
2498
+ str(self.attr_name("send_email_to_action_event")),
2499
+ str(editable),
2500
+ str(email),
2501
+ al_send_button_id,
2502
+ str(template_name),
2503
+ str(key),
2504
+ formats_value,
2494
2505
  )
2495
2506
  send_button = action_button_html(
2496
2507
  javascript_string,
@@ -2560,22 +2571,15 @@ class ALDocumentBundle(DAList):
2560
2571
  al_email_input_id = "_ignore_al_doc_email_" + name
2561
2572
  al_send_button_id = "al_send_email_button_" + name
2562
2573
 
2563
- if isinstance(preferred_formats, (list, tuple)):
2564
- formats_js = "[" + ",".join(f"'{fmt}'" for fmt in preferred_formats) + "]"
2565
- elif preferred_formats:
2566
- formats_js = f"'{preferred_formats}'"
2567
- else:
2568
- formats_js = "null"
2569
-
2570
- javascript_string = (
2571
- f"javascript:aldocument_send_action("
2572
- f"'{self.attr_name('send_email_action_event')}',"
2573
- f"'{al_wants_editable_input_id}',"
2574
- f"'{al_email_input_id}',"
2575
- f"'{template_name}',"
2576
- f"'{key}',"
2577
- f"{formats_js}"
2578
- ")"
2574
+ formats_value = [str(fmt) for fmt in preferred_formats]
2575
+ javascript_string = _javascript_href(
2576
+ "aldocument_send_action",
2577
+ str(self.attr_name("send_email_action_event")),
2578
+ al_wants_editable_input_id,
2579
+ al_email_input_id,
2580
+ str(template_name),
2581
+ str(key),
2582
+ formats_value,
2579
2583
  )
2580
2584
 
2581
2585
  # Container of whole email section with header
@@ -275,12 +275,18 @@ class ALAddress(Address):
275
275
  }
276
276
  )
277
277
 
278
+ # All 'field' entries start with the full name of the object (i.e. 'users[1].address...'),
279
+ # so make sure the keys in maxlengths and required have that prefix
280
+ prefix = self.attr_name("")
281
+ rename_key = lambda key: key if key.startswith(prefix) else prefix + key
278
282
  if maxlengths:
283
+ maxlengths = {rename_key(k): v for k, v in maxlengths.items()}
279
284
  for field in fields:
280
285
  if field["field"] in maxlengths:
281
286
  field["maxlength"] = maxlengths[field["field"]]
282
287
 
283
288
  if required:
289
+ required = {rename_key(k): v for k, v in required.items()}
284
290
  for field in fields:
285
291
  if field["field"] in required:
286
292
  field["required"] = required[field["field"]]
@@ -1223,17 +1229,6 @@ class ALIndividual(Individual):
1223
1229
  for field in fields:
1224
1230
  field["show if"] = show_if
1225
1231
 
1226
- if maxlengths:
1227
- for field in fields:
1228
- if field["field"] in maxlengths:
1229
- field["maxlength"] = maxlengths[field["field"]]
1230
-
1231
- if required:
1232
- for field in fields:
1233
- if field["field"] in required:
1234
- field["required"] = required[field["field"]]
1235
-
1236
- return fields
1237
1232
  elif person_or_business == "business":
1238
1233
  fields = [
1239
1234
  {
@@ -1244,17 +1239,6 @@ class ALIndividual(Individual):
1244
1239
  if show_if:
1245
1240
  fields[0]["show if"] = show_if
1246
1241
 
1247
- if maxlengths:
1248
- for field in fields:
1249
- if field["field"] in maxlengths:
1250
- field["maxlength"] = maxlengths[field["field"]]
1251
-
1252
- if required:
1253
- for field in fields:
1254
- if field["field"] in required:
1255
- field["required"] = required[field["field"]]
1256
-
1257
- return fields
1258
1242
  else:
1259
1243
  # Note: the labels are template block objects: if they are keys,
1260
1244
  # they should be converted to strings first
@@ -1317,17 +1301,23 @@ class ALIndividual(Individual):
1317
1301
  }
1318
1302
  )
1319
1303
 
1320
- if maxlengths:
1321
- for field in fields:
1322
- if field["field"] in maxlengths:
1323
- field["maxlength"] = maxlengths[field["field"]]
1304
+ # All 'field' entries start with the full name of the object (i.e. 'users[1].address...'),
1305
+ # so make sure the keys in maxlengths and required have that prefix
1306
+ prefix = self.attr_name("name.")
1307
+ rename_key = lambda key: key if key.startswith(prefix) else prefix + key
1308
+ if maxlengths:
1309
+ maxlengths = {rename_key(k): v for k, v in maxlengths.items()}
1310
+ for field in fields:
1311
+ if field["field"] in maxlengths:
1312
+ field["maxlength"] = maxlengths[field["field"]]
1324
1313
 
1325
- if required:
1326
- for field in fields:
1327
- if field["field"] in required:
1328
- field["required"] = required[field["field"]]
1314
+ if required:
1315
+ required = {rename_key(k): v for k, v in required.items()}
1316
+ for field in fields:
1317
+ if field["field"] in required:
1318
+ field["required"] = required[field["field"]]
1329
1319
 
1330
- return fields
1320
+ return fields
1331
1321
 
1332
1322
  def address_fields(
1333
1323
  self,
@@ -1428,12 +1418,18 @@ class ALIndividual(Individual):
1428
1418
  if show_if:
1429
1419
  fields[0]["show if"] = show_if
1430
1420
 
1421
+ # All 'field' entries start with the full name of the object (i.e. 'users[1].address...'),
1422
+ # so make sure the keys in maxlengths and required have that prefix
1423
+ prefix = self.attr_name("")
1424
+ rename_key = lambda key: key if key.startswith(prefix) else prefix + key
1431
1425
  if maxlengths:
1426
+ maxlengths = {rename_key(k): v for k, v in maxlengths.items()}
1432
1427
  for field in fields:
1433
1428
  if field["field"] in maxlengths:
1434
1429
  field["maxlength"] = maxlengths[field["field"]]
1435
1430
 
1436
1431
  if required:
1432
+ required = {rename_key(k): v for k, v in required.items()}
1437
1433
  for field in fields:
1438
1434
  if field["field"] in required:
1439
1435
  field["required"] = required[field["field"]]
@@ -1590,12 +1586,18 @@ class ALIndividual(Individual):
1590
1586
  if show_if:
1591
1587
  fields[0]["show if"] = show_if
1592
1588
 
1589
+ # All 'field' entries start with the full name of the object (i.e. 'users[1].address...'),
1590
+ # so make sure the keys in maxlengths and required have that prefix
1591
+ prefix = self.attr_name("")
1592
+ rename_key = lambda key: key if key.startswith(prefix) else prefix + key
1593
1593
  if maxlengths:
1594
+ maxlengths = {rename_key(k): v for k, v in maxlengths.items()}
1594
1595
  for field in fields:
1595
1596
  if field["field"] in maxlengths:
1596
1597
  field["maxlength"] = maxlengths[field["field"]]
1597
1598
 
1598
1599
  if required:
1600
+ required = {rename_key(k): v for k, v in required.items()}
1599
1601
  for field in fields:
1600
1602
  if field["field"] in required:
1601
1603
  field["required"] = required[field["field"]]
@@ -195,7 +195,7 @@ code: |
195
195
  if user_logged_in():
196
196
  new_session_id = save_interview_answers(
197
197
  metadata={"title": al_sessions_snapshot_label},
198
- additional_variables_to_filter=al_sessions_additional_variables_to_filter,
198
+ additional_variables_to_filter=showifdef('al_sessions_additional_variables_to_filter', []),
199
199
  )
200
200
  log(f"Saved interview {al_sessions_snapshot_label} with id {new_session_id}")
201
201
  else:
@@ -1773,7 +1773,7 @@ content: |
1773
1773
  --------------|-----------
1774
1774
  Date of email | ${current_datetime().format_datetime()}
1775
1775
  Pages | ${al_num_package_pages }
1776
- Preferred language | ${ user_preferred_language if user_needs_interpreter else 'English' }
1776
+ Preferred language | ${ user_preferred_language if showifdef('user_needs_interpreter') else 'English' }
1777
1777
  Mobile phone | ${showifdef('users[0].mobile_number')}
1778
1778
  Other phone | ${showifdef('users[0].phone_number')}
1779
1779
  Email | ${showifdef('users[0].email')}
@@ -0,0 +1,17 @@
1
+ ---
2
+ include:
3
+ - assembly_line.yml
4
+ ---
5
+ mandatory: True
6
+ code: |
7
+ al_sessions_additional_variables_to_filter = []
8
+ al_sessions_snapshot_label
9
+ al_sessions_save_session_snapshot
10
+ al_sessions_save_status
11
+ finish_screen
12
+ ---
13
+ id: finish screen
14
+ event: finish_screen
15
+ question: |
16
+ Done
17
+ ---
@@ -0,0 +1,10 @@
1
+ @snapshots
2
+ Feature: Test saving snapshots
3
+
4
+ Scenario: User can save an answer snapshot
5
+ Given I log in with "DA_ADMIN_EMAIL" and "DA_ADMIN_PASSWORD"
6
+ And I start the interview at "test_saving_snapshots"
7
+ And I should see the phrase "Save an answer set"
8
+ And I set the var "al_sessions_snapshot_label" to "My test snapshot"
9
+ And I tap to continue
10
+ And I should see the phrase "Your answer set was successfully saved"
@@ -31,9 +31,8 @@ from docassemble.base.util import (
31
31
  validation_error,
32
32
  word,
33
33
  )
34
- from docassemble.webapp.db_object import init_sqlalchemy
35
34
  from sqlalchemy.sql import text
36
- from docassemble.base.functions import server, safe_json, serializable_dict
35
+ from docassemble.base.functions import safe_json, serializable_dict
37
36
  from .al_document import (
38
37
  ALDocument,
39
38
  ALDocumentBundle,
@@ -47,6 +46,33 @@ import re
47
46
  import hashlib
48
47
  import struct
49
48
 
49
+ try:
50
+ from docassemble.base.hooks import write_answer_json as _write_answer_json
51
+ except ModuleNotFoundError as err:
52
+ if err.name != "docassemble.base.hooks":
53
+ raise
54
+ # docassemble < 1.10 exposes webapp hooks through the legacy server object.
55
+ from docassemble.base.functions import server as _legacy_server
56
+
57
+ def _write_answer_json(*args, **kwargs):
58
+ return _legacy_server.write_answer_json(*args, **kwargs)
59
+
60
+
61
+ try:
62
+ from docassemble.webapp.db import (
63
+ get_session as _get_session,
64
+ session_scope as _session_scope,
65
+ )
66
+ except ModuleNotFoundError as err:
67
+ if err.name != "docassemble.webapp.db":
68
+ raise
69
+ # docassemble < 1.10 uses a SQLAlchemy engine instead of session context managers.
70
+ from docassemble.webapp.db_object import init_sqlalchemy
71
+
72
+ _legacy_db = init_sqlalchemy()
73
+ _get_session = _legacy_db.connect
74
+ _session_scope = _legacy_db.begin
75
+
50
76
  try:
51
77
  import zoneinfo # type: ignore
52
78
  except ImportError:
@@ -80,8 +106,6 @@ __all__ = [
80
106
  "update_session_metadata",
81
107
  ]
82
108
 
83
- db = init_sqlalchemy()
84
-
85
109
  al_sessions_variables_to_remove: Set = {
86
110
  # Internal fields
87
111
  "_internal",
@@ -278,7 +302,7 @@ def set_interview_metadata(
278
302
  data (Dict): The metadata to add.
279
303
  metadata_key_name (str, optional): The name of the metadata key. Defaults to "metadata".
280
304
  """
281
- server.write_answer_json(
305
+ _write_answer_json(
282
306
  session_id, filename, safe_json(data), tags=metadata_key_name, persistent=True
283
307
  )
284
308
 
@@ -304,8 +328,8 @@ def get_interview_metadata(
304
328
  AND tags = :tags
305
329
  AND key = :session_id
306
330
  """)
307
- with db.connect() as con:
308
- row = con.execute(
331
+ with _get_session() as session:
332
+ row = session.execute(
309
333
  sql,
310
334
  {"filename": filename, "tags": metadata_key_name, "session_id": session_id},
311
335
  ).fetchone()
@@ -448,8 +472,8 @@ def get_saved_interview_list(
448
472
  return []
449
473
 
450
474
  sessions = []
451
- with db.connect() as con:
452
- rs = con.execute(
475
+ with _get_session() as session:
476
+ rs = session.execute(
453
477
  get_sessions_query,
454
478
  {
455
479
  "metadata": metadata_key_name,
@@ -464,8 +488,8 @@ def get_saved_interview_list(
464
488
  ), # We need to pass a value to the query, but it's treated as a flag
465
489
  },
466
490
  )
467
- for session in rs:
468
- sessions.append(dict(session._mapping))
491
+ for row in rs:
492
+ sessions.append(dict(row._mapping))
469
493
 
470
494
  return sessions
471
495
 
@@ -666,11 +690,11 @@ def find_matching_sessions(
666
690
  parameters[f"{column}_filter"] = val_tuple[0]
667
691
 
668
692
  sessions = []
669
- with db.connect() as con:
670
- rs = con.execute(get_sessions_query, parameters)
693
+ with _get_session() as session:
694
+ rs = session.execute(get_sessions_query, parameters)
671
695
 
672
- for session in rs:
673
- sessions.append(dict(session._mapping))
696
+ for row in rs:
697
+ sessions.append(dict(row._mapping))
674
698
 
675
699
  return sessions
676
700
 
@@ -720,8 +744,8 @@ def delete_interview_sessions(
720
744
 
721
745
  log(f"Deleting sessions with {user_id} {filename_to_exclude} {current_filename}")
722
746
 
723
- with db.connect() as connection:
724
- connection.execute(
747
+ with _session_scope() as session:
748
+ session.execute(
725
749
  delete_sessions_query,
726
750
  {
727
751
  "user_id": user_id,
@@ -1349,6 +1373,7 @@ def get_filtered_session_variables(
1349
1373
  all_vars = {k: v for k, v in all_vars.items() if k not in variables_to_filter}
1350
1374
 
1351
1375
  items_to_check = list(all_vars.items())
1376
+ visited = set()
1352
1377
 
1353
1378
  while items_to_check:
1354
1379
  key, value = items_to_check.pop()
@@ -1358,6 +1383,10 @@ def get_filtered_session_variables(
1358
1383
  del all_vars[key]
1359
1384
  continue
1360
1385
 
1386
+ if id(value) in visited:
1387
+ continue
1388
+ visited.add(id(value))
1389
+
1361
1390
  if isinstance(value, DAObject):
1362
1391
  # docassemble overrides both __dir__ and __getattr__ for reasons unknown
1363
1392
  # we need to use the base Python versions to get what we expect
@@ -1638,11 +1667,11 @@ def get_filenames_having_sessions(
1638
1667
  sql_all = text("SELECT DISTINCT filename FROM userdict")
1639
1668
  sql_user = text("SELECT DISTINCT filename FROM userdict WHERE user_id = :user_id")
1640
1669
 
1641
- with db.connect() as conn:
1670
+ with _get_session() as session:
1642
1671
  if user_id is None:
1643
- rows = conn.execute(sql_all).mappings().all()
1672
+ rows = session.execute(sql_all).mappings().all()
1644
1673
  else:
1645
- rows = conn.execute(sql_user, {"user_id": user_id}).mappings().all()
1674
+ rows = session.execute(sql_user, {"user_id": user_id}).mappings().all()
1646
1675
 
1647
1676
  return [row["filename"] for row in rows]
1648
1677
 
@@ -1723,49 +1752,48 @@ def update_session_metadata(
1723
1752
  h1 = to_signed_32(high_u32)
1724
1753
  h2 = to_signed_32(low_u32)
1725
1754
 
1726
- with db.connect() as con:
1727
- # Wrap in a transaction so the advisory lock holds until COMMIT
1728
- with con.begin():
1729
- # 3) Acquire the advisory lock on (h1,h2)
1730
- con.execute(
1731
- text("SELECT pg_advisory_xact_lock(:h1, :h2)"),
1732
- {"h1": h1, "h2": h2},
1733
- )
1755
+ # The advisory lock and upsert must share one transaction.
1756
+ with _session_scope() as session:
1757
+ # 3) Acquire the advisory lock on (h1,h2)
1758
+ session.execute(
1759
+ text("SELECT pg_advisory_xact_lock(:h1, :h2)"),
1760
+ {"h1": h1, "h2": h2},
1761
+ )
1734
1762
 
1735
- # 4) Try UPDATE first, using CAST() instead of ::jsonb
1736
- update_sql = text("""
1737
- UPDATE jsonstorage
1738
- SET data = jsonstorage.data || CAST(:data AS jsonb)
1739
- WHERE key = :session_id
1740
- AND filename = :filename
1741
- AND tags = :tags
1763
+ # 4) Try UPDATE first, using CAST() instead of ::jsonb
1764
+ update_sql = text("""
1765
+ UPDATE jsonstorage
1766
+ SET data = jsonstorage.data || CAST(:data AS jsonb)
1767
+ WHERE key = :session_id
1768
+ AND filename = :filename
1769
+ AND tags = :tags
1770
+ """)
1771
+ result = session.execute(
1772
+ update_sql,
1773
+ {
1774
+ "data": json_data_string,
1775
+ "session_id": session_id,
1776
+ "filename": filename,
1777
+ "tags": metadata_key_name,
1778
+ },
1779
+ )
1780
+
1781
+ # 5) If nothing was updated, INSERT
1782
+ if (result.rowcount or 0) == 0:
1783
+ insert_sql = text("""
1784
+ INSERT INTO jsonstorage (key, filename, tags, data)
1785
+ VALUES (:session_id, :filename, :tags, CAST(:data AS jsonb))
1742
1786
  """)
1743
- result = con.execute(
1744
- update_sql,
1787
+ session.execute(
1788
+ insert_sql,
1745
1789
  {
1746
- "data": json_data_string,
1747
1790
  "session_id": session_id,
1748
1791
  "filename": filename,
1749
1792
  "tags": metadata_key_name,
1793
+ "data": json_data_string,
1750
1794
  },
1751
1795
  )
1752
1796
 
1753
- # 5) If nothing was updated, INSERT
1754
- if (result.rowcount or 0) == 0:
1755
- insert_sql = text("""
1756
- INSERT INTO jsonstorage (key, filename, tags, data)
1757
- VALUES (:session_id, :filename, :tags, CAST(:data AS jsonb))
1758
- """)
1759
- con.execute(
1760
- insert_sql,
1761
- {
1762
- "session_id": session_id,
1763
- "filename": filename,
1764
- "tags": metadata_key_name,
1765
- "data": json_data_string,
1766
- },
1767
- )
1768
-
1769
1797
 
1770
1798
  def update_current_session_metadata(
1771
1799
  data: Dict[str, Any],
@@ -1,8 +1,37 @@
1
1
  # do not pre-load
2
2
 
3
+ import json
3
4
  import unittest
5
+ from html import unescape
4
6
  from docassemble.base.util import DAFile, DATemplate
5
- from .al_document import ALDocument, ALDocumentBundle, ALAddendumField
7
+ from .al_document import ALDocument, ALDocumentBundle, ALAddendumField, _javascript_href
8
+
9
+
10
+ class TestJavascriptHref(unittest.TestCase):
11
+ def test_escapes_javascript_strings_inside_html_href(self):
12
+ result = _javascript_href(
13
+ "aldocument_send_action",
14
+ "template_request.requestee_bundles['4167e36be7f04794ad30770e865afe68']",
15
+ "_ignore_al_wants_editable_bundle",
16
+ "_ignore_al_doc_email_bundle",
17
+ )
18
+
19
+ self.assertEqual(
20
+ result,
21
+ "javascript:aldocument_send_action("
22
+ "&quot;template_request.requestee_bundles[&#x27;4167e36be7f04794ad30770e865afe68&#x27;]&quot;,"
23
+ "&quot;_ignore_al_wants_editable_bundle&quot;,"
24
+ "&quot;_ignore_al_doc_email_bundle&quot;)",
25
+ )
26
+
27
+ def test_preserves_javascript_value_types(self):
28
+ result = unescape(_javascript_href("send", "None", None, ["pdf", "docx"]))
29
+ args_json = result.removeprefix("javascript:send(").removesuffix(")")
30
+
31
+ self.assertEqual(
32
+ json.loads(f"[{args_json}]"),
33
+ ["None", None, ["pdf", "docx"]],
34
+ )
6
35
 
7
36
 
8
37
  class test_dont_assume_pdf(unittest.TestCase):
@@ -25,6 +25,10 @@ class test_aladdress(unittest.TestCase):
25
25
  )
26
26
  addr.instanceName = "addr"
27
27
  fields = addr.address_fields(required={"addr.zip": True})
28
+ for field in fields:
29
+ if "zip" in field["field"]:
30
+ self.assertTrue(field["required"])
31
+ fields = addr.address_fields(required={"zip": True})
28
32
  for field in fields:
29
33
  if "zip" in field["field"]:
30
34
  self.assertTrue(field["required"])
@@ -586,7 +590,7 @@ class TestALIndividual(unittest.TestCase):
586
590
  # Test business case with required parameter
587
591
  fields = self.individual.name_fields(
588
592
  person_or_business="business",
589
- required={"test_individual.name.first": True},
593
+ required={"first": True},
590
594
  title_choices=["a title"],
591
595
  suffix_choices=["Jr.", "Sr."],
592
596
  )
@@ -613,9 +617,7 @@ class TestALIndividual(unittest.TestCase):
613
617
  ) # Should not have required by default
614
618
 
615
619
  # Test with required parameter
616
- fields = self.individual.gender_fields(
617
- required={"test_individual.gender": True}
618
- )
620
+ fields = self.individual.gender_fields(required={"gender": True})
619
621
  gender_field = fields[0]
620
622
  self.assertEqual(gender_field["required"], True)
621
623
 
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassemble.AssemblyLine
3
- Version: 4.6.2
3
+ Version: 4.7.0
4
4
  Summary: Quickly go from a paper court form to a runnable, guided, step-by-step web application powered by Docassemble. Swap out branding and pre-built questions to meet your needs.
5
5
  Author-email: Suffolk Legal Innovation and Technology Lab <litlab@suffolk.edu>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://courtformsonline.org
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
- Requires-Dist: docassemble.ALToolbox>=0.16.0
10
+ Requires-Dist: docassemble.ALToolbox>=0.18.0
11
11
  Requires-Dist: docassemble.GitHubFeedbackForm>=0.4.1.1
12
12
  Dynamic: license-file
13
13
 
@@ -55,6 +55,7 @@ docassemble/AssemblyLine/data/questions/test_if_final.yml
55
55
  docassemble/AssemblyLine/data/questions/test_preferred_formats.yml
56
56
  docassemble/AssemblyLine/data/questions/test_preferred_name.yml
57
57
  docassemble/AssemblyLine/data/questions/test_question_library.yml
58
+ docassemble/AssemblyLine/data/questions/test_saving_snapshots.yml
58
59
  docassemble/AssemblyLine/data/questions/test_signature.yml
59
60
  docassemble/AssemblyLine/data/sources/README.md
60
61
  docassemble/AssemblyLine/data/sources/ar-words.yml
@@ -68,6 +69,7 @@ docassemble/AssemblyLine/data/sources/test_alexhibit_jpg_1.jpg
68
69
  docassemble/AssemblyLine/data/sources/test_alexhibit_pdf_1.pdf
69
70
  docassemble/AssemblyLine/data/sources/test_alexhibit_png_1.png
70
71
  docassemble/AssemblyLine/data/sources/test_question_library.feature
72
+ docassemble/AssemblyLine/data/sources/test_snapshots.feature
71
73
  docassemble/AssemblyLine/data/sources/translation_es.xlsx
72
74
  docassemble/AssemblyLine/data/sources/translation_ht.xlsx
73
75
  docassemble/AssemblyLine/data/sources/translation_pt.xlsx
@@ -1,2 +1,2 @@
1
- docassemble.ALToolbox>=0.16.0
1
+ docassemble.ALToolbox>=0.18.0
2
2
  docassemble.GitHubFeedbackForm>=0.4.1.1
@@ -6,14 +6,14 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "docassemble.AssemblyLine"
9
- version = "4.6.2"
9
+ version = "4.7.0"
10
10
  description = "Quickly go from a paper court form to a runnable, guided, step-by-step web application powered by Docassemble. Swap out branding and pre-built questions to meet your needs."
11
11
  readme = "README.md"
12
12
  authors = [
13
13
  { name="Suffolk Legal Innovation and Technology Lab", email="litlab@suffolk.edu" },
14
14
  ]
15
15
  dependencies = [
16
- "docassemble.ALToolbox>=0.16.0",
16
+ "docassemble.ALToolbox>=0.18.0",
17
17
  "docassemble.GitHubFeedbackForm>=0.4.1.1",
18
18
  ]
19
19
  license="MIT"
@@ -76,3 +76,5 @@ dev = [
76
76
  "types-psycopg2",
77
77
  ]
78
78
 
79
+ [tool.uv]
80
+ exclude-newer-package = {"docassemble.ALToolbox" = false}
@@ -1 +0,0 @@
1
- __version__ = '4.6.2'