docassemble.AssemblyLine 4.0.0__tar.gz → 4.2.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 (97) hide show
  1. {docassemble_assemblyline-4.0.0/docassemble.AssemblyLine.egg-info → docassemble_assemblyline-4.2.0}/PKG-INFO +2 -2
  2. docassemble_assemblyline-4.2.0/docassemble/AssemblyLine/__init__.py +1 -0
  3. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/al_courts.py +12 -11
  4. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/al_general.py +97 -48
  5. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/custom_jinja_filters.py +112 -2
  6. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_document.yml +2 -2
  7. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/ql_baseline.yml +39 -4
  8. docassemble_assemblyline-4.2.0/docassemble/AssemblyLine/data/questions/test_if_final.yml +27 -0
  9. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/aldocument.js +24 -3
  10. docassemble_assemblyline-4.2.0/docassemble/AssemblyLine/data/templates/test_if_final.docx +0 -0
  11. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/sessions.py +39 -45
  12. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/test_al_general.py +151 -0
  13. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0/docassemble.AssemblyLine.egg-info}/PKG-INFO +2 -2
  14. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble.AssemblyLine.egg-info/SOURCES.txt +3 -1
  15. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble.AssemblyLine.egg-info/requires.txt +1 -1
  16. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/setup.py +2 -2
  17. docassemble_assemblyline-4.0.0/docassemble/AssemblyLine/__init__.py +0 -1
  18. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/LICENSE +0 -0
  19. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/MANIFEST.in +0 -0
  20. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/README.md +0 -0
  21. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/al_document.py +0 -0
  22. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_code.yml +0 -0
  23. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_language.yml +0 -0
  24. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_package.yml +0 -0
  25. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_package_unstyled.yml +0 -0
  26. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_question_test.yml +0 -0
  27. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_reminders.yml +0 -0
  28. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_saved_sessions.yml +0 -0
  29. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_saved_sessions_store.yml +0 -0
  30. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_settings.yml +0 -0
  31. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_terms_of_use.yml +0 -0
  32. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/al_visual.yml +0 -0
  33. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/assembly_line.yml +0 -0
  34. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/assembly_line_unstyled.yml +0 -0
  35. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/demo_search_sessions.yml +0 -0
  36. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/feedback.yml +0 -0
  37. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/interview_list.yml +0 -0
  38. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/ql_catchall.yml +0 -0
  39. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/ql_namechange.yml +0 -0
  40. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/reserved_words.yml +0 -0
  41. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_al_table_document.yml +0 -0
  42. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_aladdendum.yml +0 -0
  43. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_alcourts.yml +0 -0
  44. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_aldocument.yml +0 -0
  45. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_aldocument_background_assembly.yml +0 -0
  46. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_alexhibit.yml +0 -0
  47. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_alexhibit_maxsize.yml +0 -0
  48. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_bundle_parity.yml +0 -0
  49. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_catchall.yml +0 -0
  50. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_familiar_disambiguation.yml +0 -0
  51. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_preferred_formats.yml +0 -0
  52. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_question_library.yml +0 -0
  53. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/questions/test_signature.yml +0 -0
  54. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/README.md +0 -0
  55. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/es-words.yml +0 -0
  56. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/languages.yml +0 -0
  57. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/test_aldocument.feature +0 -0
  58. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_docx_1.docx +0 -0
  59. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_jpg_1.jpg +0 -0
  60. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_pdf_1.pdf +0 -0
  61. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_png_1.png +0 -0
  62. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/test_question_library.feature +0 -0
  63. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/translation_es.xlsx +0 -0
  64. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/translation_ht.xlsx +0 -0
  65. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/sources/translation_pt.xlsx +0 -0
  66. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/README.md +0 -0
  67. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/al_audio.css +0 -0
  68. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/al_audio.js +0 -0
  69. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/aldocument.css +0 -0
  70. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/interview_list.css +0 -0
  71. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/limit_upload_size.js +0 -0
  72. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/lt1-pullout-10-getting-organized.pdf +0 -0
  73. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/ma_logo.png +0 -0
  74. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/placeholder.png +0 -0
  75. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/seal.jpg +0 -0
  76. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/static/styles.css +0 -0
  77. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/templates/README.md +0 -0
  78. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/templates/al_basic_addendum.docx +0 -0
  79. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/templates/exhibit_cover_sheet.docx +0 -0
  80. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/templates/exhibit_table_of_contents.docx +0 -0
  81. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/templates/general_cover_sheet.docx +0 -0
  82. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/templates/sample_word_template.docx +0 -0
  83. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/templates/test_aldocument_docx_1.docx +0 -0
  84. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/templates/test_aldocument_pdf_1.pdf +0 -0
  85. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/data/templates/test_catchall.docx +0 -0
  86. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/language.py +0 -0
  87. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/py.typed +0 -0
  88. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/requirements.txt +0 -0
  89. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/sign.py +0 -0
  90. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/test_al_document.py +0 -0
  91. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/AssemblyLine/test_language.py +0 -0
  92. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble/__init__.py +0 -0
  93. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble.AssemblyLine.egg-info/dependency_links.txt +0 -0
  94. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble.AssemblyLine.egg-info/not-zip-safe +0 -0
  95. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/docassemble.AssemblyLine.egg-info/top_level.txt +0 -0
  96. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/pyproject.toml +0 -0
  97. {docassemble_assemblyline-4.0.0 → docassemble_assemblyline-4.2.0}/setup.cfg +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassemble.AssemblyLine
3
- Version: 4.0.0
3
+ Version: 4.2.0
4
4
  Home-page: https://courtformsonline.org
5
5
  Author: Suffolk Legal Innovation and Technology Lab
6
6
  Author-email: qsteenhuis@suffolk.edu
7
7
  License: The MIT License (MIT)
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
- Requires-Dist: docassemble.ALToolbox>=0.11.1
10
+ Requires-Dist: docassemble.ALToolbox>=0.12.0
11
11
  Requires-Dist: docassemble.GithubFeedbackForm>=0.4.1.1
12
12
  Dynamic: author
13
13
  Dynamic: author-email
@@ -0,0 +1 @@
1
+ __version__ = '4.2.0'
@@ -3,7 +3,7 @@ Package for a very simple / MVP list of courts that is mostly signature compatib
3
3
  """
4
4
 
5
5
  import os
6
- from typing import Any, Callable, Dict, List, Mapping, Optional, Union, Set
6
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Union, Set, Tuple
7
7
  import pandas as pd
8
8
  import docassemble.base.functions
9
9
  from docassemble.base.util import (
@@ -238,12 +238,12 @@ class ALCourtLoader(DAObject):
238
238
  # Only solution I can think of would require court database owners to assign each court a unique ID
239
239
  # and something that triggers recalculating the court address/etc info.
240
240
 
241
- def all_courts(self) -> List[Dict[int, str]]:
241
+ def all_courts(self) -> List[Tuple[int, str]]:
242
242
  """
243
243
  Return a list of all courts in the spreadsheet.
244
244
 
245
245
  Returns:
246
- List[Dict[int, str]]: List of all ALCourt instances without filtering.
246
+ List[Tuple[int, str]]: List of tuples where each tuple contains (dataframe_index, display_value). The dataframe_index (int) can be used with as_court() to retrieve the full court object. The display_value (str) is the court's name or other display column value.
247
247
  """
248
248
  return self.filter_courts(None)
249
249
 
@@ -329,13 +329,14 @@ class ALCourtLoader(DAObject):
329
329
  display_column: str = "name",
330
330
  search_string: Optional[str] = None,
331
331
  search_columns: Optional[Union[List[str], str]] = None,
332
- ) -> List[Dict[int, str]]:
332
+ ) -> List[Tuple[int, str]]:
333
333
  """
334
334
  Retrieve a list of all courts in the specified county.
335
335
 
336
336
  This function fetches courts suitable for displaying as a drop-down or radio button list
337
- in Docassemble. The results are dictionaries where the key is the index in the dataframe,
338
- useful for retrieving the court's full details later using the as_court() method.
337
+ in Docassemble. The results are tuples where the first element is the dataframe index
338
+ (useful for retrieving the court's full details later using the as_court() method) and
339
+ the second element is the display value from the specified display_column.
339
340
 
340
341
  Args:
341
342
  county_name (str): Name of the county.
@@ -346,7 +347,7 @@ class ALCourtLoader(DAObject):
346
347
  the search_string in a case-insensitive manner. Defaults to None.
347
348
 
348
349
  Returns:
349
- List[Dict[int, str]]: List of dictionaries representing matching courts.
350
+ List[Tuple[int, str]]: List of tuples where each tuple contains (dataframe_index, display_value). The dataframe_index (int) can be used with as_court() to retrieve the full court object. The display_value (str) is the court's name or other display column value.
350
351
  """
351
352
  return self.filter_courts(
352
353
  court_types=county_name,
@@ -363,11 +364,11 @@ class ALCourtLoader(DAObject):
363
364
  display_column: str = "name",
364
365
  search_string: Optional[str] = None,
365
366
  search_columns: Optional[Union[List[str], str]] = None,
366
- ) -> List[Dict[int, str]]:
367
+ ) -> List[Tuple[int, str]]:
367
368
  """
368
- Return a filtered subset of courts represented as a list of dictionaries.
369
+ Return a filtered subset of courts represented as a list of tuples.
369
370
 
370
- Each dictionary has the format {index: name}, where "index" refers to the dataframe index and "name"
371
+ Each tuple has the format (index, display_value), where "index" refers to the dataframe index and "display_value"
371
372
  is determined by the `display_column`.
372
373
 
373
374
  Args:
@@ -380,7 +381,7 @@ class ALCourtLoader(DAObject):
380
381
  the search_string in a case-insensitive manner. Defaults to None.
381
382
 
382
383
  Returns:
383
- List[Dict[int, str]]: List of dictionaries representing filtered courts.
384
+ List[Tuple[int, str]]: List of tuples where each tuple contains (dataframe_index, display_value). The dataframe_index (int) can be used with as_court() to retrieve the full court object. The display_value (str) is the court's name or other display column value.
384
385
  """
385
386
  df = self._load_courts()
386
387
  if court_types:
@@ -37,6 +37,7 @@ from docassemble.base.util import (
37
37
  log,
38
38
  value,
39
39
  )
40
+ from docassemble.ALToolbox.misc import fa_icon
40
41
  import random
41
42
  import re
42
43
  import pycountry
@@ -802,12 +803,12 @@ class ALPeopleList(DAList):
802
803
  [person.familiar(**kwargs) for person in self], and_string=word("or")
803
804
  )
804
805
 
805
- def short_list(self, limit: int, truncate_string: str = ", et. al.") -> str:
806
- """Return a subset of the list, truncated with 'et. al.' if it exceeds a given limit.
806
+ def short_list(self, limit: int, truncate_string: str = ", et al.") -> str:
807
+ """Return a subset of the list, truncated with 'et al.' if it exceeds a given limit.
807
808
 
808
809
  Args:
809
810
  limit (int): The maximum number of items to display before truncating.
810
- truncate_string (str, optional): The string to append when truncating. Defaults to ', et. al.'.
811
+ truncate_string (str, optional): The string to append when truncating. Defaults to ', et al.'.
811
812
 
812
813
  Returns:
813
814
  str: Formatted string of names, truncated if needed.
@@ -958,6 +959,14 @@ class ALIndividual(Individual):
958
959
  ) -> str:
959
960
  """Fetches and formats the phone numbers of the individual.
960
961
 
962
+ Supports the following attributes:
963
+
964
+ - `mobile_number`: Mobile phone number
965
+ - `phone_number`: Other phone number
966
+ - `work_number`: Work phone number
967
+ - `other_number`: Any other phone number
968
+ - `home_number`: Home phone number (if applicable)
969
+
961
970
  Args:
962
971
  country (str, optional): The country for phone number formatting. Defaults to the country of the docassemble server.
963
972
  show_impounded (bool): If True, shows the phone numbers even if impounded. Defaults to False.
@@ -986,6 +995,36 @@ class ALIndividual(Individual):
986
995
  nums.append({fmt_number: "other"})
987
996
  else:
988
997
  nums.append({self.phone_number: "other"})
998
+ if hasattr(self, "work_number") and self.work_number:
999
+ fmt_number = None
1000
+ try:
1001
+ fmt_number = phone_number_formatted(self.work_number, country=country)
1002
+ except:
1003
+ fmt_number = None
1004
+ if fmt_number:
1005
+ nums.append({fmt_number: "work"})
1006
+ else:
1007
+ nums.append({self.work_number: "work"})
1008
+ if hasattr(self, "other_number") and self.other_number:
1009
+ fmt_number = None
1010
+ try:
1011
+ fmt_number = phone_number_formatted(self.other_number, country=country)
1012
+ except:
1013
+ fmt_number = None
1014
+ if fmt_number:
1015
+ nums.append({fmt_number: "other"})
1016
+ else:
1017
+ nums.append({self.other_number: "other"})
1018
+ if hasattr(self, "home_number") and self.home_number:
1019
+ fmt_number = None
1020
+ try:
1021
+ fmt_number = phone_number_formatted(self.home_number, country=country)
1022
+ except:
1023
+ fmt_number = None
1024
+ if fmt_number:
1025
+ nums.append({fmt_number: "home"})
1026
+ else:
1027
+ nums.append({self.home_number: "home"})
989
1028
  if len(nums) < 1:
990
1029
  return ""
991
1030
  # Check for impounded phone number
@@ -1075,6 +1114,7 @@ class ALIndividual(Individual):
1075
1114
  maxlengths: Optional[Dict[str, int]] = None,
1076
1115
  suffix_choices: Optional[Union[List[str], Callable]] = None,
1077
1116
  title_options: Optional[Union[List[str], Callable]] = None,
1117
+ required: Optional[Dict[str, bool]] = None,
1078
1118
  ) -> List[Dict[str, str]]:
1079
1119
  """
1080
1120
  Generates suitable field prompts for a name based on the type of entity (person or business)
@@ -1094,6 +1134,7 @@ class ALIndividual(Individual):
1094
1134
  maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
1095
1135
  suffix_choices (Union[List[str], Callable], optional): A list of suffix options or a callable to generate suffix options, or overridden by value of global `al_name_suffixes`.
1096
1136
  title_options: (Union[List[str], Callable], optional): Deprecated parameter, use `title_choices` instead. If provided, it will be used to set the title choices.
1137
+ required (Dict[str, bool], optional): A dictionary of field names and if they should be required. Default is None.
1097
1138
 
1098
1139
  Returns:
1099
1140
  List[Dict[str, str]]: A list of dictionaries where each dictionary contains field prompt details.
@@ -1157,6 +1198,17 @@ class ALIndividual(Individual):
1157
1198
  if show_if:
1158
1199
  for field in fields:
1159
1200
  field["show if"] = show_if
1201
+
1202
+ if maxlengths:
1203
+ for field in fields:
1204
+ if field["field"] in maxlengths:
1205
+ field["maxlength"] = maxlengths[field["field"]]
1206
+
1207
+ if required:
1208
+ for field in fields:
1209
+ if field["field"] in required:
1210
+ field["required"] = required[field["field"]]
1211
+
1160
1212
  return fields
1161
1213
  elif person_or_business == "business":
1162
1214
  fields = [
@@ -1167,6 +1219,17 @@ class ALIndividual(Individual):
1167
1219
  ]
1168
1220
  if show_if:
1169
1221
  fields[0]["show if"] = show_if
1222
+
1223
+ if maxlengths:
1224
+ for field in fields:
1225
+ if field["field"] in maxlengths:
1226
+ field["maxlength"] = maxlengths[field["field"]]
1227
+
1228
+ if required:
1229
+ for field in fields:
1230
+ if field["field"] in required:
1231
+ field["required"] = required[field["field"]]
1232
+
1170
1233
  return fields
1171
1234
  else:
1172
1235
  # Note: the labels are template block objects: if they are keys,
@@ -1234,6 +1297,12 @@ class ALIndividual(Individual):
1234
1297
  for field in fields:
1235
1298
  if field["field"] in maxlengths:
1236
1299
  field["maxlength"] = maxlengths[field["field"]]
1300
+
1301
+ if required:
1302
+ for field in fields:
1303
+ if field["field"] in required:
1304
+ field["required"] = required[field["field"]]
1305
+
1237
1306
  return fields
1238
1307
 
1239
1308
  def address_fields(
@@ -1285,6 +1354,7 @@ class ALIndividual(Individual):
1285
1354
  show_if: Union[str, Dict[str, str], None] = None,
1286
1355
  maxlengths: Optional[Dict[str, int]] = None,
1287
1356
  choices: Optional[Union[List[Dict[str, str]], Callable]] = None,
1357
+ required: Optional[Dict[str, bool]] = None,
1288
1358
  ) -> List[Dict[str, str]]:
1289
1359
  """
1290
1360
  Generate fields for capturing gender information, including a
@@ -1295,6 +1365,7 @@ class ALIndividual(Individual):
1295
1365
  show_if (Union[str, Dict[str, str], None]): Condition to determine if the field should be shown. Defaults to None.
1296
1366
  maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
1297
1367
  choices (Optional[Union[List[Dict[str, str]], Callable]]): A list of choices of genders to use in the prompts, or a callable that returns such a list. Default set of choices includes male, female, nonbinary, prefer-not-to-say, self-described, and unknown.
1368
+ required (Dict[str, bool], optional): A dictionary of field names and if they should be required. Default is None.
1298
1369
 
1299
1370
  Returns:
1300
1371
  List[Dict[str, str]]: A list of dictionaries with field prompts for gender.
@@ -1338,13 +1409,18 @@ class ALIndividual(Individual):
1338
1409
  if field["field"] in maxlengths:
1339
1410
  field["maxlength"] = maxlengths[field["field"]]
1340
1411
 
1412
+ if required:
1413
+ for field in fields:
1414
+ if field["field"] in required:
1415
+ field["required"] = required[field["field"]]
1416
+
1341
1417
  return fields
1342
1418
 
1343
1419
  def pronoun_fields(
1344
1420
  self,
1345
1421
  show_help=False,
1346
1422
  show_if: Union[str, Dict[str, str], None] = None,
1347
- required: bool = False,
1423
+ required: Union[bool, Dict[str, bool]] = False,
1348
1424
  shuffle: bool = False,
1349
1425
  show_unknown: Optional[Union[Literal["guess"], bool]] = "guess",
1350
1426
  maxlengths: Optional[Dict[str, int]] = None,
@@ -1356,7 +1432,7 @@ class ALIndividual(Individual):
1356
1432
  Args:
1357
1433
  show_help (bool): Whether to show additional help text. Defaults to False.
1358
1434
  show_if (Union[str, Dict[str, str], None]): Condition to determine if the field should be shown. Defaults to None.
1359
- required (bool): Whether the field is required. Defaults to False.
1435
+ required (Union[bool, Dict[str, bool]]): Whether the field is required. Can be a boolean (applies to all fields) or a dictionary of field names and if they should be required. Defaults to False.
1360
1436
  shuffle (bool): Whether to shuffle the order of pronouns. Defaults to False.
1361
1437
  show_unknown (Union[Literal["guess"], bool]): Whether to show an "unknown" option. Can be "guess", True, or False. Defaults to "guess".
1362
1438
  maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
@@ -1390,7 +1466,7 @@ class ALIndividual(Individual):
1390
1466
  "datatype": "checkboxes",
1391
1467
  "choices": shuffled_choices + final_choices,
1392
1468
  "none of the above": str(self.pronoun_prefer_not_to_say_label),
1393
- "required": required,
1469
+ "required": required if isinstance(required, bool) else False,
1394
1470
  },
1395
1471
  self_described_input,
1396
1472
  ]
@@ -1405,6 +1481,12 @@ class ALIndividual(Individual):
1405
1481
  if field["field"] in maxlengths:
1406
1482
  field["maxlength"] = maxlengths[field["field"]]
1407
1483
 
1484
+ # Handle dictionary-based required parameter
1485
+ if isinstance(required, dict):
1486
+ for field in fields:
1487
+ if field["field"] in required:
1488
+ field["required"] = required[field["field"]]
1489
+
1408
1490
  return fields
1409
1491
 
1410
1492
  def get_pronouns(self) -> set:
@@ -1443,6 +1525,7 @@ class ALIndividual(Individual):
1443
1525
  style: str = "radio",
1444
1526
  show_if: Union[str, Dict[str, str], None] = None,
1445
1527
  maxlengths: Optional[Dict[str, int]] = None,
1528
+ required: Optional[Dict[str, bool]] = None,
1446
1529
  ) -> List[Dict[str, str]]:
1447
1530
  """
1448
1531
  Generate fields for capturing language preferences.
@@ -1452,6 +1535,7 @@ class ALIndividual(Individual):
1452
1535
  style (str): The display style of choices. Defaults to "radio".
1453
1536
  show_if (Union[str, Dict[str, str], None]): Condition to determine if the field should be shown. Defaults to None.
1454
1537
  maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
1538
+ required (Dict[str, bool], optional): A dictionary of field names and if they should be required. Default is None.
1455
1539
 
1456
1540
  Returns:
1457
1541
  List[Dict[str, str]]: A list of dictionaries with field prompts for language preferences.
@@ -1486,6 +1570,12 @@ class ALIndividual(Individual):
1486
1570
  for field in fields:
1487
1571
  if field["field"] in maxlengths:
1488
1572
  field["maxlength"] = maxlengths[field["field"]]
1573
+
1574
+ if required:
1575
+ for field in fields:
1576
+ if field["field"] in required:
1577
+ field["required"] = required[field["field"]]
1578
+
1489
1579
  return fields
1490
1580
 
1491
1581
  def language_name(self) -> str:
@@ -2322,47 +2412,6 @@ def filter_letters(letter_strings: Union[List[str], str]) -> str:
2322
2412
  # Note: removed "combined_locations" because it is too tightly coupled to MACourts.py right now
2323
2413
 
2324
2414
 
2325
- def fa_icon(
2326
- icon: str, color: str = "primary", color_css: Optional[str] = None, size: str = "sm"
2327
- ) -> str:
2328
- """
2329
- Return HTML for a font-awesome icon of the specified size and color. You can reference
2330
- a CSS variable (such as Bootstrap theme color) or a true CSS color reference, such as 'blue' or
2331
- '#DDDDDD'. Defaults to Bootstrap theme color "primary".
2332
-
2333
- Args:
2334
- icon (str): The name of the icon to use. See https://fontawesome.com/icons for a list of icons.
2335
- color (str): The color of the icon. Defaults to "primary".
2336
- color_css (Optional[str]): A CSS variable or color reference. Defaults to None.
2337
- size (str): The size of the icon. Defaults to "sm".
2338
-
2339
- Returns:
2340
- str: HTML for the icon.
2341
- """
2342
- if not color and not color_css:
2343
- return ":" + icon + ":" # Default to letting Docassemble handle it
2344
- elif color_css:
2345
- return (
2346
- '<i class="fa fa-'
2347
- + icon
2348
- + " fa-"
2349
- + size
2350
- + '" style="color:'
2351
- + color_css
2352
- + ';"></i>'
2353
- )
2354
- else:
2355
- return (
2356
- '<i class="fa fa-'
2357
- + icon
2358
- + " fa-"
2359
- + size
2360
- + '" style="color:var(--'
2361
- + color
2362
- + ');"></i>'
2363
- )
2364
-
2365
-
2366
2415
  def is_sms_enabled() -> bool:
2367
2416
  """Checks if SMS (Twilio) is enabled on the server. Does not verify that it works.
2368
2417
 
@@ -2404,7 +2453,7 @@ def is_phone_or_email(text: str) -> bool:
2404
2453
  DAValidationError if the string is neither a valid phone number nor a valid email address.
2405
2454
  """
2406
2455
  sms_enabled = is_sms_enabled()
2407
- if re.match("\S+@\S+", text) or (sms_enabled and phone_number_is_valid(text)):
2456
+ if re.match(r"\S+@\S+", text) or (sms_enabled and phone_number_is_valid(text)):
2408
2457
  return True
2409
2458
  else:
2410
2459
  if sms_enabled:
@@ -5,8 +5,11 @@
5
5
 
6
6
  # See: https://docassemble.org/docs/documents.html#register_jinja_filter
7
7
 
8
- from typing import Any, Dict, List
9
- from docassemble.base.util import register_jinja_filter, DACatchAll
8
+ import re
9
+ from typing import Any, Dict, List, Optional, Union
10
+ from docassemble.base.util import register_jinja_filter, DACatchAll, word
11
+ from jinja2 import Undefined, pass_context
12
+ from jinja2.runtime import Context as Jinja2Context
10
13
 
11
14
  __all__ = [
12
15
  "catchall_options",
@@ -200,11 +203,118 @@ def catchall_subquestion(value: Any, subquestion: str) -> DACatchAll:
200
203
  return value
201
204
 
202
205
 
206
+ def _undefined_label(ud: Undefined) -> Union[str, None]:
207
+ """
208
+ Extract the variable name from a Jinja2 Undefined object for use as a placeholder label.
209
+
210
+ Args:
211
+ ud (Undefined): The Jinja2 Undefined object.
212
+ Returns:
213
+ str: A friendly name extracted from the Undefined object.
214
+ """
215
+ return getattr(ud, "_undefined_name", None)
216
+
217
+
218
+ @pass_context
219
+ def if_final(
220
+ context: Jinja2Context,
221
+ value: Any,
222
+ i: Optional[str] = None,
223
+ expected_i: Union[str, List[str]] = "final",
224
+ placeholder: Optional[str] = None,
225
+ ) -> Any:
226
+ """
227
+ Jinja2 filter to only seek the definition of a variable if the current value of `i`
228
+ is equal to the expected value (normally "final"); otherwise,
229
+ return a placeholder.
230
+
231
+ This is useful in ALDocument DOCX templates where you want to show a placeholder when
232
+ the document is being generated for preview or testing, but let Docassemble trigger
233
+ the actual value when the document is being generated for final output.
234
+
235
+ E.g., to show a placeholder for a signature field when the document is being
236
+ shown to the signer, but show the actual signature when the document is finalized.
237
+
238
+ The default placeholder is "[ signature ]" if the variable name follows the pattern "users[0].signature",
239
+ or [ variable_name ] if it is not an attribute of an item.
240
+
241
+ `i` will be the value from the template's context unless it is explicitly passed,
242
+ as in an ALDocument's "preview" or "final" values.
243
+
244
+ Example:
245
+ Contents of test_if_final.docx:
246
+ ```jinja
247
+ {{ users[0].signature | if_final }}
248
+ ```
249
+
250
+ Returns "[ signature ]" if `i` (passed to the context of the attachment block) is not "final",
251
+ otherwise the actual value of `users[0].signature`.
252
+
253
+ ```yaml
254
+ ---
255
+ include:
256
+ - assembly_line.yml
257
+ ---
258
+ mandatory: True
259
+ code: |
260
+ preview_screen
261
+ final_screen
262
+ ---
263
+ question: |
264
+ Here is what it looks like unsigned
265
+ subquestion: |
266
+ ${ test_if_final_attachment.as_pdf(key="preview") }
267
+ continue button field: preview_screen
268
+ ---
269
+ question: |
270
+ Here is what it looks like signed
271
+ subquestion: |
272
+ ${ test_if_final_attachment.as_pdf(key="final") }
273
+ event: final_screen
274
+ ---
275
+ objects:
276
+ - test_if_final_attachment: ALDocument.using(title="test_if_final", filename="test_if_final")
277
+ ---
278
+ attachment:
279
+ variable name: test_if_final_attachment[i]
280
+ docx template file: test_if_final.docx
281
+ ```
282
+
283
+ Args:
284
+ context (Jinja2Context): The Jinja2 context, automatically passed by the `pass_context` decorator.
285
+ value (Any): The original value as passed to the filter.
286
+ i (str, optional): The current value of `i`. If not provided, it will be fetched from the context.
287
+ expected_i (Union[str, List[str]], optional): The expected value(s) of `i` to trigger passthrough of `value`.
288
+ Defaults to "final".
289
+ placeholder (str, optional): The placeholder string to return if the condition is not met. If
290
+ not provided, a default placeholder will be generated based on the variable name.
291
+
292
+ Returns:
293
+ Any: The original `value` if `i` matches `expected_i`, otherwise the `placeholder`.
294
+ """
295
+ if i is None:
296
+ i = context.get("i")
297
+
298
+ if i != expected_i if isinstance(expected_i, str) else i not in expected_i:
299
+ if isinstance(value, Undefined):
300
+ _label = _undefined_label(value)
301
+ if _label and not placeholder:
302
+ if "." in _label:
303
+ _label = _label.split(".")[-1]
304
+ placeholder = f"[ {_label} ]"
305
+ return placeholder
306
+ return value
307
+
308
+ # Allow Docassemble to trap the UndefinedError when i == expected_i (e.g., "final" instead of "preview"), triggering a question to define `value`
309
+ return value
310
+
311
+
203
312
  register_jinja_filter("catchall_options", catchall_options)
204
313
  register_jinja_filter("catchall_label", catchall_label)
205
314
  register_jinja_filter("catchall_datatype", catchall_datatype)
206
315
  register_jinja_filter("catchall_question", catchall_question)
207
316
  register_jinja_filter("catchall_subquestion", catchall_subquestion)
317
+ register_jinja_filter("if_final", if_final)
208
318
 
209
319
 
210
320
  def catchall_fields_code(value: Any) -> List[Dict[str, Any]]:
@@ -162,7 +162,7 @@ subquestion: |
162
162
  This can take a few minutes.
163
163
 
164
164
  <div class="spinner-border text-primary d-flex justify-content-center" role="status">
165
- <span class="sr-only">Making documents...</span>
165
+ <span class="sr-only visually-hidden">Making documents...</span>
166
166
  </div>
167
167
  event: al_download_waiting_screen
168
168
  reload: True
@@ -192,7 +192,7 @@ subquestion: |
192
192
  This can take a few minutes.
193
193
 
194
194
  <div class="spinner-border text-primary d-flex justify-content-center" role="status">
195
- <span class="sr-only">Generating preview...</span>
195
+ <span class="sr-only visually-hidden">Generating preview...</span>
196
196
  </div>
197
197
  event: al_preview_waiting_screen
198
198
  reload: True
@@ -1173,6 +1173,30 @@ fields:
1173
1173
  - Phone: x.mobile_number
1174
1174
  datatype: al_international_phone
1175
1175
  ---
1176
+ id: persons home number
1177
+ generic object: ALIndividual
1178
+ question: |
1179
+ What is ${x}'s home phone number?
1180
+ fields:
1181
+ - Phone: x.home_number
1182
+ datatype: al_international_phone
1183
+ ---
1184
+ id: persons work number
1185
+ generic object: ALIndividual
1186
+ question: |
1187
+ What is ${x}'s work phone number?
1188
+ fields:
1189
+ - Phone: x.work_number
1190
+ datatype: al_international_phone
1191
+ ---
1192
+ id: persons other number
1193
+ generic object: ALIndividual
1194
+ question: |
1195
+ What is ${x}'s other phone number?
1196
+ fields:
1197
+ - Phone: x.other_number
1198
+ datatype: al_international_phone
1199
+ ---
1176
1200
  id: persons fax number
1177
1201
  generic object: ALIndividual
1178
1202
  question: |
@@ -1440,6 +1464,17 @@ code: |
1440
1464
  code: |
1441
1465
  signature_fields = ['users[0].signature']
1442
1466
  ---
1467
+ code: |
1468
+ if len(signature_fields):
1469
+ if next(iter(signature_fields)).endswith(".signature"):
1470
+ # the id: signature choice question has to assume the object exists, so we don't guard against that here
1471
+ # note: this does not need to be an ALIndividual, so just use str() instead of .full_name()
1472
+ slash_s_example_name = str(value( next(iter(signature_fields))[:-len(".signature")] ))
1473
+ else:
1474
+ slash_s_example_name = "Signer's Name"
1475
+ else:
1476
+ slash_s_example_name = "Signer's Name"
1477
+ ---
1443
1478
  id: signature choice
1444
1479
  decoration: file-signature
1445
1480
  question: |
@@ -1458,7 +1493,7 @@ fields:
1458
1493
  We can share a link with your phone on the next screen
1459
1494
  % endif
1460
1495
  - On this device: this_device
1461
- - With a typed signature, like "${ al_typed_signature_prefix }${ users[0] }": typed_signature
1496
+ - With a typed signature, like "${ al_typed_signature_prefix }${ slash_s_example_name }": typed_signature
1462
1497
  show if:
1463
1498
  code: |
1464
1499
  al_form_requires_digital_signature and not (device() and (device().is_mobile or device().is_touch_capable))
@@ -1467,7 +1502,7 @@ fields:
1467
1502
  input type: radio
1468
1503
  choices:
1469
1504
  - On this device: this_device
1470
- - With a typed signature, like "${ al_typed_signature_prefix }${ users[0] }": typed_signature
1505
+ - With a typed signature, like "${ al_typed_signature_prefix }${ slash_s_example_name }": typed_signature
1471
1506
  show if:
1472
1507
  code: |
1473
1508
  al_form_requires_digital_signature and (device() and (device().is_mobile or device().is_touch_capable))
@@ -1483,7 +1518,7 @@ fields:
1483
1518
  We can share a link with your phone on the next screen
1484
1519
  % endif
1485
1520
  - On this device: this_device
1486
- - With a typed signature, like "${ al_typed_signature_prefix }${ users[0] }": typed_signature
1521
+ - With a typed signature, like "${ al_typed_signature_prefix }${ slash_s_example_name }": typed_signature
1487
1522
  - On the paper with a pen after I print the documents: sign_after_printing
1488
1523
  show if:
1489
1524
  code: |
@@ -1493,7 +1528,7 @@ fields:
1493
1528
  input type: radio
1494
1529
  choices:
1495
1530
  - On this device: this_device
1496
- - With a typed signature, like "${ al_typed_signature_prefix }${ users[0] }": typed_signature
1531
+ - With a typed signature, like "${ al_typed_signature_prefix }${ slash_s_example_name }": typed_signature
1497
1532
  - On the paper with a pen after I print the documents: sign_after_printing
1498
1533
  show if:
1499
1534
  code: |
@@ -0,0 +1,27 @@
1
+ ---
2
+ include:
3
+ - assembly_line.yml
4
+ ---
5
+ mandatory: True
6
+ code: |
7
+ preview_screen
8
+ final_screen
9
+ ---
10
+ question: |
11
+ Here is what it looks like unsigned
12
+ subquestion: |
13
+ ${ test_if_final_attachment.as_pdf(key="preview") }
14
+ continue button field: preview_screen
15
+ ---
16
+ question: |
17
+ Here is what it looks like signed
18
+ subquestion: |
19
+ ${ test_if_final_attachment.as_pdf(key="final") }
20
+ event: final_screen
21
+ ---
22
+ objects:
23
+ - test_if_final_attachment: ALDocument.using(title="test_if_final", filename="test_if_final")
24
+ ---
25
+ attachment:
26
+ variable name: test_if_final_attachment[i]
27
+ docx template file: test_if_final.docx