docassemble.AssemblyLine 3.5.0__tar.gz → 4.0.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-3.5.0/docassemble.AssemblyLine.egg-info → docassemble_assemblyline-4.0.0}/PKG-INFO +1 -1
  2. docassemble_assemblyline-4.0.0/docassemble/AssemblyLine/__init__.py +1 -0
  3. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/al_document.py +138 -42
  4. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/al_general.py +58 -49
  5. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/custom_jinja_filters.py +109 -1
  6. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_document.yml +29 -3
  7. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_saved_sessions.yml +4 -7
  8. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_visual.yml +4 -6
  9. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/ql_baseline.yml +87 -22
  10. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/ql_catchall.yml +26 -5
  11. docassemble_assemblyline-4.0.0/docassemble/AssemblyLine/data/questions/test_catchall.yml +45 -0
  12. docassemble_assemblyline-4.0.0/docassemble/AssemblyLine/data/questions/test_preferred_formats.yml +75 -0
  13. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_question_library.yml +25 -2
  14. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/test_question_library.feature +1 -1
  15. docassemble_assemblyline-4.0.0/docassemble/AssemblyLine/data/sources/translation_es.xlsx +0 -0
  16. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/aldocument.js +11 -5
  17. docassemble_assemblyline-4.0.0/docassemble/AssemblyLine/data/templates/test_catchall.docx +0 -0
  18. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/sessions.py +107 -3
  19. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/test_al_general.py +4 -3
  20. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/test_language.py +12 -13
  21. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0/docassemble.AssemblyLine.egg-info}/PKG-INFO +1 -1
  22. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble.AssemblyLine.egg-info/SOURCES.txt +4 -2
  23. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/setup.py +1 -1
  24. docassemble_assemblyline-3.5.0/docassemble/AssemblyLine/__init__.py +0 -1
  25. docassemble_assemblyline-3.5.0/docassemble/AssemblyLine/data/sources/translation_es.xlsx +0 -0
  26. docassemble_assemblyline-3.5.0/docassemble/AssemblyLine/project_maintenance.py +0 -432
  27. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/LICENSE +0 -0
  28. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/MANIFEST.in +0 -0
  29. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/README.md +0 -0
  30. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/al_courts.py +0 -0
  31. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_code.yml +0 -0
  32. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_language.yml +0 -0
  33. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_package.yml +0 -0
  34. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_package_unstyled.yml +0 -0
  35. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_question_test.yml +0 -0
  36. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_reminders.yml +0 -0
  37. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_saved_sessions_store.yml +0 -0
  38. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_settings.yml +0 -0
  39. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/al_terms_of_use.yml +0 -0
  40. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/assembly_line.yml +0 -0
  41. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/assembly_line_unstyled.yml +0 -0
  42. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/demo_search_sessions.yml +0 -0
  43. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/feedback.yml +0 -0
  44. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/interview_list.yml +0 -0
  45. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/ql_namechange.yml +0 -0
  46. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/reserved_words.yml +0 -0
  47. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_al_table_document.yml +0 -0
  48. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_aladdendum.yml +0 -0
  49. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_alcourts.yml +0 -0
  50. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_aldocument.yml +0 -0
  51. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_aldocument_background_assembly.yml +0 -0
  52. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_alexhibit.yml +0 -0
  53. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_alexhibit_maxsize.yml +0 -0
  54. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_bundle_parity.yml +0 -0
  55. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_familiar_disambiguation.yml +0 -0
  56. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/questions/test_signature.yml +0 -0
  57. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/README.md +0 -0
  58. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/es-words.yml +0 -0
  59. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/languages.yml +0 -0
  60. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/test_aldocument.feature +0 -0
  61. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_docx_1.docx +0 -0
  62. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_jpg_1.jpg +0 -0
  63. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_pdf_1.pdf +0 -0
  64. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/test_alexhibit_png_1.png +0 -0
  65. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/translation_ht.xlsx +0 -0
  66. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/sources/translation_pt.xlsx +0 -0
  67. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/README.md +0 -0
  68. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/al_audio.css +0 -0
  69. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/al_audio.js +0 -0
  70. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/aldocument.css +0 -0
  71. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/interview_list.css +0 -0
  72. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/limit_upload_size.js +0 -0
  73. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/lt1-pullout-10-getting-organized.pdf +0 -0
  74. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/ma_logo.png +0 -0
  75. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/placeholder.png +0 -0
  76. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/seal.jpg +0 -0
  77. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/static/styles.css +0 -0
  78. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/templates/README.md +0 -0
  79. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/templates/al_basic_addendum.docx +0 -0
  80. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/templates/exhibit_cover_sheet.docx +0 -0
  81. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/templates/exhibit_table_of_contents.docx +0 -0
  82. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/templates/general_cover_sheet.docx +0 -0
  83. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/templates/sample_word_template.docx +0 -0
  84. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/templates/test_aldocument_docx_1.docx +0 -0
  85. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/data/templates/test_aldocument_pdf_1.pdf +0 -0
  86. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/language.py +0 -0
  87. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/py.typed +0 -0
  88. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/requirements.txt +0 -0
  89. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/sign.py +0 -0
  90. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/AssemblyLine/test_al_document.py +0 -0
  91. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble/__init__.py +0 -0
  92. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble.AssemblyLine.egg-info/dependency_links.txt +0 -0
  93. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble.AssemblyLine.egg-info/not-zip-safe +0 -0
  94. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble.AssemblyLine.egg-info/requires.txt +0 -0
  95. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/docassemble.AssemblyLine.egg-info/top_level.txt +0 -0
  96. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/pyproject.toml +0 -0
  97. {docassemble_assemblyline-3.5.0 → docassemble_assemblyline-4.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassemble.AssemblyLine
3
- Version: 3.5.0
3
+ Version: 4.0.0
4
4
  Home-page: https://courtformsonline.org
5
5
  Author: Suffolk Legal Innovation and Technology Lab
6
6
  Author-email: qsteenhuis@suffolk.edu
@@ -0,0 +1 @@
1
+ __version__ = '4.0.0'
@@ -36,6 +36,9 @@ import subprocess
36
36
  from collections import ChainMap
37
37
  import pikepdf
38
38
  from typing import Tuple
39
+ import secrets
40
+ import string
41
+
39
42
 
40
43
  __all__ = [
41
44
  "ALAddendumField",
@@ -59,6 +62,22 @@ __all__ = [
59
62
  DEBUG_MODE = get_config("debug")
60
63
 
61
64
 
65
+ def random_suffix(length: int = 8) -> str:
66
+ """Return a random string for use in unique IDs.
67
+
68
+ Note: this is powerful enough for the expected usecase of distinguishing a few
69
+ HTML elements from each other, but not cryptographically secure or as strong as
70
+ a true GUID.
71
+
72
+ Args:
73
+ length (int): The length of the random string to generate. Defaults to 8.
74
+ Returns:
75
+ str: A random string of lowercase letters and digits.
76
+ """
77
+ alphabet = string.ascii_lowercase + string.digits
78
+ return "".join(secrets.choice(alphabet) for _ in range(length))
79
+
80
+
62
81
  def base_name(filename: str) -> str:
63
82
  """
64
83
  Extracts the base name of a file without its extension.
@@ -1653,8 +1672,8 @@ class ALDocumentBundle(DAList):
1653
1672
  refresh: bool = True,
1654
1673
  pdfa: bool = False,
1655
1674
  title: str = "",
1656
- format="pdf",
1657
- include_pdf=True,
1675
+ format: Optional[str] = "pdf",
1676
+ include_pdf: Optional[bool] = True,
1658
1677
  ) -> DAFile:
1659
1678
  """
1660
1679
  Returns a zip file containing all enabled documents in the bundle in the specified format.
@@ -1670,6 +1689,11 @@ class ALDocumentBundle(DAList):
1670
1689
  Returns:
1671
1690
  DAFile: A zip file containing the enabled documents.
1672
1691
  """
1692
+ if format is None:
1693
+ format = "pdf"
1694
+
1695
+ if include_pdf is None:
1696
+ include_pdf = True
1673
1697
 
1674
1698
  zip_key = f"{ space_to_underscore( key )}_zip"
1675
1699
 
@@ -1679,6 +1703,7 @@ class ALDocumentBundle(DAList):
1679
1703
 
1680
1704
  # strip out a possible '.pdf' ending then add '.zip'
1681
1705
  zipname = os.path.splitext(self.filename)[0]
1706
+
1682
1707
  if format == "docx":
1683
1708
  docs = []
1684
1709
  for doc in self.enabled_documents(refresh=refresh):
@@ -1864,6 +1889,8 @@ class ALDocumentBundle(DAList):
1864
1889
  include_zip: bool = True,
1865
1890
  include_full_pdf: bool = False,
1866
1891
  append_matching_suffix: bool = True,
1892
+ zip_include_pdf: Optional[bool] = None,
1893
+ zip_format: Optional[str] = None,
1867
1894
  ) -> Tuple[List[Dict[str, DAFile]], Optional[DAFile], Optional[DAFile]]:
1868
1895
  """
1869
1896
  Generates a cache of all enabled documents in the bundle, and returns it in a structure that can be cached
@@ -1887,6 +1914,8 @@ class ALDocumentBundle(DAList):
1887
1914
  include_zip (bool): Flag to include a zip option, default is True.
1888
1915
  include_full_pdf (bool): Flag to include a PDF version of the whole bundle, default is False.
1889
1916
  append_matching_suffix (bool): Flag to determine if matching suffix should be appended to file name, default is True.
1917
+ zip_include_pdf (Optional[bool]): If True, includes a PDF version in the zip file even if original is in DOCX format.
1918
+ zip_format (Optional[str]): Format of the primary version of each document.
1890
1919
 
1891
1920
  Returns:
1892
1921
  Tuple[List[Dict[str, DAFile]], Optional[DAFile], Optional[DAFile]]: A list of dictionaries containing the enabled documents, a zip file of the whole bundle, and a PDF of the whole
@@ -1940,9 +1969,14 @@ class ALDocumentBundle(DAList):
1940
1969
  pass
1941
1970
  results.append(result)
1942
1971
 
1972
+ if zip_format is None:
1973
+ zip_format = "original" if original else "docx" if docx else "pdf"
1974
+
1943
1975
  if len(enabled_docs) > 1 and include_zip:
1944
1976
  bundled_zip = self.as_zip(
1945
- key=key, format="original" if original else "docx" if docx else "pdf"
1977
+ key=key,
1978
+ format=zip_format,
1979
+ include_pdf=zip_include_pdf,
1946
1980
  )
1947
1981
  else:
1948
1982
  bundled_zip = None
@@ -1976,6 +2010,8 @@ class ALDocumentBundle(DAList):
1976
2010
  use_previously_cached_files: bool = False,
1977
2011
  include_full_pdf: bool = False,
1978
2012
  full_pdf_label: Optional[str] = None,
2013
+ zip_include_pdf: Optional[bool] = True,
2014
+ zip_format: Optional[str] = None,
1979
2015
  ) -> str:
1980
2016
  """
1981
2017
  Constructs an HTML table displaying a list of documents with 'view' and 'download' buttons.
@@ -2002,6 +2038,8 @@ class ALDocumentBundle(DAList):
2002
2038
  use_previously_cached_files (bool): Flag to use previously cached files (e.g., made in background) if defined. default is False.
2003
2039
  include_full_pdf (bool): Flag to include a full PDF option, default is False.
2004
2040
  full_pdf_label (Optional[str]): Label for the full PDF option. If not provided, uses the generic template for `self.full_pdf_label` ("Download all").
2041
+ zip_include_pdf (Optional[bool]): Flag to include PDF files in the zip archive, default is True. If `None` value of `view` will be used.
2042
+ zip_format (Optional[str]): Format of the files in the zip archive. If None, defaults to value of "format" parameter.
2005
2043
 
2006
2044
  Returns:
2007
2045
  str: HTML representation of a table with documents and their associated actions.
@@ -2012,6 +2050,12 @@ class ALDocumentBundle(DAList):
2012
2050
  if not hasattr(self, "_cached_full_pdf_label"):
2013
2051
  self._cached_full_pdf_label = str(self.full_pdf_label)
2014
2052
 
2053
+ if zip_format is None:
2054
+ zip_format = format
2055
+
2056
+ if zip_include_pdf is None:
2057
+ zip_include_pdf = view
2058
+
2015
2059
  if use_previously_cached_files and hasattr(self, "_downloadable_files"):
2016
2060
  downloadable_files, bundled_zip, bundled_pdf = self._downloadable_files
2017
2061
  else:
@@ -2025,6 +2069,8 @@ class ALDocumentBundle(DAList):
2025
2069
  include_zip=include_zip,
2026
2070
  include_full_pdf=include_full_pdf,
2027
2071
  append_matching_suffix=append_matching_suffix,
2072
+ zip_include_pdf=zip_include_pdf,
2073
+ zip_format=zip_format,
2028
2074
  )
2029
2075
 
2030
2076
  html = f'<div class="container al_table al_doc_table" id="{ html_safe_str(self.instanceName) }">'
@@ -2215,7 +2261,7 @@ class ALDocumentBundle(DAList):
2215
2261
  self._cached_include_editable_documents = str(
2216
2262
  self.include_editable_documents
2217
2263
  )
2218
- name = html_safe_str(self.instanceName)
2264
+ name = html_safe_str(self.instanceName) + random_suffix()
2219
2265
  al_wants_editable_input_id = "_ignore_al_wants_editable_" + name
2220
2266
  al_email_input_id = "_ignore_al_doc_email_" + name
2221
2267
  al_send_button_id = "al_send_email_button_" + name
@@ -2250,12 +2296,13 @@ class ALDocumentBundle(DAList):
2250
2296
  def send_button_to_html(
2251
2297
  self,
2252
2298
  email: str,
2253
- editable: bool = False,
2299
+ editable: Optional[bool] = None,
2254
2300
  template_name: str = "",
2255
2301
  label: str = "Send",
2256
2302
  icon: str = "envelope",
2257
2303
  color: str = "primary",
2258
2304
  key: str = "final",
2305
+ preferred_formats: Optional[Union[str, List[str]]] = None,
2259
2306
  ) -> str:
2260
2307
  """
2261
2308
  Generate HTML for a button that allows someone to send the bundle to a
@@ -2264,12 +2311,13 @@ class ALDocumentBundle(DAList):
2264
2311
 
2265
2312
  Args:
2266
2313
  email (str): The recipient's email address.
2267
- editable (bool, optional): Flag indicating if the bundle is editable. Defaults to False.
2314
+ editable (bool, optional): Flag indicating if the bundle is editable. Defaults to False. (deprecated; use preferred_formats instead)
2268
2315
  template_name (str, optional): The name of the template to be used. Defaults to an empty string.
2269
2316
  label (str, optional): The label for the button. Defaults to "Send".
2270
2317
  icon (str, optional): The Fontawesome icon for the button. Defaults to "envelope".
2271
2318
  color (str, optional): The Bootstrap color of the button. Defaults to "primary".
2272
2319
  key (str, optional): A key used to identify which version of the ALDocument to send. Defaults to "final".
2320
+ preferred_formats (Optional[Union[str,List[str]]], optional): A list of allowed formats for the document. Defaults to "pdf" if not specified.
2273
2321
 
2274
2322
  Returns:
2275
2323
  str: The generated HTML string for the button.
@@ -2278,9 +2326,16 @@ class ALDocumentBundle(DAList):
2278
2326
  return "" # Don't let people email an empty set of documents
2279
2327
  if not hasattr(self, "_cached_get_email_copy"):
2280
2328
  self._cached_get_email_copy = str(self.get_email_copy)
2281
- name = html_safe_str(self.instanceName)
2329
+ name = html_safe_str(self.instanceName) + random_suffix()
2282
2330
  al_send_button_id = "al_send_email_to_button_" + name
2283
2331
 
2332
+ if isinstance(preferred_formats, (list, tuple)):
2333
+ formats_js = "[" + ",".join(f"'{fmt}'" for fmt in preferred_formats) + "]"
2334
+ elif preferred_formats:
2335
+ formats_js = f"'{preferred_formats}'"
2336
+ else:
2337
+ formats_js = "null"
2338
+
2284
2339
  javascript_string = (
2285
2340
  f"javascript:aldocument_send_to_action("
2286
2341
  f"'{self.attr_name('send_email_to_action_event')}',"
@@ -2288,7 +2343,9 @@ class ALDocumentBundle(DAList):
2288
2343
  f"'{email}',"
2289
2344
  f"'{al_send_button_id}',"
2290
2345
  f"'{template_name}',"
2291
- f"'{key}')"
2346
+ f"'{key}',"
2347
+ f"{formats_js}"
2348
+ f")"
2292
2349
  )
2293
2350
  send_button = action_button_html(
2294
2351
  javascript_string,
@@ -2309,6 +2366,7 @@ class ALDocumentBundle(DAList):
2309
2366
  template_name: str = "",
2310
2367
  label: str = "Send",
2311
2368
  icon: str = "envelope",
2369
+ preferred_formats: Optional[Union[str, List[str]]] = None,
2312
2370
  ) -> str:
2313
2371
  """
2314
2372
  Generate HTML for an input box and button that allows someone to send the bundle
@@ -2321,12 +2379,13 @@ class ALDocumentBundle(DAList):
2321
2379
  key (str, optional): A key used to identify which version of the ALDocument to send. Defaults to "final".
2322
2380
  show_editable_checkbox (bool, optional): Flag indicating if the checkbox
2323
2381
  for deciding the inclusion of an editable (Word) copy should be displayed.
2324
- Defaults to True.
2382
+ Defaults to True. If preferred_formats = ["pdf"], this will be ignored and no checkbox will be shown.
2325
2383
  template_name (str, optional): Name of the template variable that is used to fill
2326
2384
  the email contents. By default, the `x.send_email_template` template will be used.
2327
2385
  label (str, optional): The label for the button. Defaults to "Send".
2328
2386
  icon (str, optional): The Fontawesome icon for the button. Defaults
2329
2387
  to "envelope".
2388
+ preferred_formats (Optional[Union[str,List[str]]], optional): A list of allowed formats for the document. Defaults to "pdf" if not specified.
2330
2389
 
2331
2390
  Returns:
2332
2391
  str: The generated HTML string for the input box and button.
@@ -2339,31 +2398,52 @@ class ALDocumentBundle(DAList):
2339
2398
  self._cached_include_editable_documents = str(
2340
2399
  self.include_editable_documents
2341
2400
  )
2342
- name = html_safe_str(self.instanceName)
2401
+
2402
+ if isinstance(preferred_formats, str):
2403
+ preferred_formats = [preferred_formats]
2404
+
2405
+ if not preferred_formats:
2406
+ preferred_formats = ["pdf", "docx"] if show_editable_checkbox else ["pdf"]
2407
+
2408
+ name = html_safe_str(self.instanceName) + random_suffix()
2343
2409
  al_wants_editable_input_id = "_ignore_al_wants_editable_" + name
2344
2410
  al_email_input_id = "_ignore_al_doc_email_" + name
2345
2411
  al_send_button_id = "al_send_email_button_" + name
2346
2412
 
2413
+ if isinstance(preferred_formats, (list, tuple)):
2414
+ formats_js = "[" + ",".join(f"'{fmt}'" for fmt in preferred_formats) + "]"
2415
+ elif preferred_formats:
2416
+ formats_js = f"'{preferred_formats}'"
2417
+ else:
2418
+ formats_js = "null"
2419
+
2347
2420
  javascript_string = (
2348
2421
  f"javascript:aldocument_send_action("
2349
2422
  f"'{self.attr_name('send_email_action_event')}',"
2350
2423
  f"'{al_wants_editable_input_id}',"
2351
2424
  f"'{al_email_input_id}',"
2352
2425
  f"'{template_name}',"
2353
- f"'{key}')"
2426
+ f"'{key}',"
2427
+ f"{formats_js}"
2428
+ ")"
2354
2429
  )
2355
2430
 
2356
2431
  # Container of whole email section with header
2357
2432
  return_str = f"""
2358
- <fieldset class="al_send_bundle al_send_section_alone {name}" id="al_send_bundle_{name}" name="al_send_bundle_{name}">
2433
+ <fieldset class="al_send_bundle al_send_section_alone {html_safe_str(self.instanceName)}" id="al_send_bundle_{name}" name="al_send_bundle_{name}">
2359
2434
  <legend class="h4 al_doc_email_header">{self._cached_get_email_copy}</legend>
2360
2435
  """
2361
2436
  # "Editable" checkbox
2362
- if show_editable_checkbox:
2437
+ if (
2438
+ show_editable_checkbox
2439
+ and preferred_formats
2440
+ and isinstance(preferred_formats, list)
2441
+ and len(preferred_formats) > 1
2442
+ ): # Do not need to show if only one valid format allowed
2363
2443
  return_str += f"""
2364
2444
  <div class="form-check-container">
2365
2445
  <div class="form-check">
2366
- <input class="form-check-input" type="checkbox" class="al_wants_editable" id="{al_wants_editable_input_id}">
2446
+ <input class="form-check-input al_wants_editable" type="checkbox" id="{al_wants_editable_input_id}">
2367
2447
  <label class="al_wants_editable form-check-label" for="{al_wants_editable_input_id}">{self._cached_include_editable_documents}
2368
2448
  </label>
2369
2449
  </div>
@@ -2373,7 +2453,7 @@ class ALDocumentBundle(DAList):
2373
2453
  return_str += f"""
2374
2454
  <div class="al_email_container">
2375
2455
 
2376
- <span class="al_email_address {name} container form-group row da-field-container da-field-container-datatype-email">
2456
+ <span class="al_email_address {html_safe_str(self.instanceName)} container form-group row da-field-container da-field-container-datatype-email">
2377
2457
  <label for="{al_email_input_id}" class="col-form-label da-form-label datext-right">Email</label>
2378
2458
  <input value="{user_info().email if user_logged_in() else ''}" alt="Email address for document" class="form-control" type="email" size="35" name="{al_email_input_id}" id="{al_email_input_id}">
2379
2459
  </span>
@@ -2389,8 +2469,9 @@ class ALDocumentBundle(DAList):
2389
2469
  self,
2390
2470
  to: Any = None,
2391
2471
  key: str = "final",
2392
- editable: bool = False,
2393
- template: Any = None,
2472
+ editable: Optional[bool] = None,
2473
+ template: Optional[Any] = None,
2474
+ preferred_formats: Optional[Union[str, List[str]]] = "pdf",
2394
2475
  **kwargs,
2395
2476
  ) -> bool:
2396
2477
  """
@@ -2399,39 +2480,54 @@ class ALDocumentBundle(DAList):
2399
2480
  https://docassemble.org/docs/functions.html#send_email with additional parameters.
2400
2481
 
2401
2482
  Args:
2402
- to (Any): The email address or list of addresses to send to. It can be a string
2403
- or objects with such. Similar to da send_email `to`.
2483
+ to (Any): The email address, list of email addresses, or list of Individuals with a .email attribute to send to.
2404
2484
  key (str, optional): Specifies which version of the document to send. Defaults to "final".
2405
- editable (bool, optional): If True, sends the editable documents. Defaults to False.
2406
- template (Any): The template variable, similar to da `send_email` `template` variable.
2485
+ editable (bool, optional): If True, sends the editable documents. Defaults to False. (Deprecated)
2486
+ template (Any): The template variable for the subject and body of the email, similar to da `send_email` `template` variable.
2487
+ preferred_formats (str): Specifies the format of the files to send. Can be "pdf" or "docx", or a list of these formats. Overrides deprecated `editable` keyword.
2407
2488
  **kwargs: Additional parameters to pass to the da `send_email` function.
2408
2489
 
2409
2490
  Returns:
2410
2491
  bool: Indicates if the email was sent successfully.
2411
2492
  """
2412
- if not template:
2493
+ if editable is not None:
2494
+ log(
2495
+ "The 'editable' parameter is deprecated; use 'preferred_formats' instead."
2496
+ )
2497
+
2498
+ if template is None:
2413
2499
  template = self.send_email_template
2414
2500
 
2415
- if editable:
2416
- return send_email(
2417
- to=to,
2418
- template=template,
2419
- # Add both DOCX and PDF versions, but if it's not possible to be a DOCX don't add the PDF
2420
- # twice
2421
- attachments=list(
2422
- dict.fromkeys(
2423
- self.as_editable_list(key=key) + self.as_pdf_list(key=key)
2424
- )
2425
- ),
2426
- **kwargs,
2427
- )
2428
- else:
2429
- return send_email(
2430
- to=to,
2431
- template=template,
2432
- attachments=self.as_pdf_list(key=key),
2433
- **kwargs,
2434
- )
2501
+ if preferred_formats is None:
2502
+ preferred_formats = ["docx", "pdf"] if editable else ["pdf"]
2503
+
2504
+ if isinstance(preferred_formats, str):
2505
+ preferred_formats = [preferred_formats]
2506
+
2507
+ allowed = {fmt.lower() for fmt in preferred_formats}
2508
+ attachments = []
2509
+
2510
+ for item in self.enabled_documents():
2511
+ primary = None
2512
+
2513
+ if "docx" in allowed:
2514
+ primary = item.as_docx(key=key)
2515
+ attachments.append(primary)
2516
+
2517
+ if "pdf" in allowed:
2518
+ if not (
2519
+ primary
2520
+ and hasattr(primary, "extension")
2521
+ and primary.extension == "pdf"
2522
+ ):
2523
+ attachments.append(item.as_pdf(key=key))
2524
+
2525
+ return send_email(
2526
+ to=to,
2527
+ template=template,
2528
+ attachments=attachments,
2529
+ **kwargs,
2530
+ )
2435
2531
 
2436
2532
  def _is_self_enabled(self, refresh=True) -> bool:
2437
2533
  """
@@ -1,5 +1,5 @@
1
1
  from copy import deepcopy
2
- from typing import Dict, List, Literal, Union, Optional, Any
2
+ from typing import Callable, Dict, List, Literal, Union, Optional, Any
3
3
  from docassemble.base.util import (
4
4
  Address,
5
5
  as_datetime,
@@ -21,7 +21,6 @@ from docassemble.base.util import (
21
21
  Individual,
22
22
  IndividualName,
23
23
  its,
24
- name_suffix,
25
24
  phone_number_formatted,
26
25
  phone_number_is_valid,
27
26
  showifdef,
@@ -32,8 +31,11 @@ from docassemble.base.util import (
32
31
  this_thread,
33
32
  url_action,
34
33
  validation_error,
34
+ value,
35
35
  word,
36
36
  your,
37
+ log,
38
+ value,
37
39
  )
38
40
  import random
39
41
  import re
@@ -1068,9 +1070,11 @@ class ALIndividual(Individual):
1068
1070
  person_or_business: str = "person",
1069
1071
  show_suffix: bool = True,
1070
1072
  show_title: bool = False,
1071
- title_options: Optional[List[str]] = None,
1073
+ title_choices: Optional[Union[List[str], Callable]] = None,
1072
1074
  show_if: Union[str, Dict[str, str], None] = None,
1073
1075
  maxlengths: Optional[Dict[str, int]] = None,
1076
+ suffix_choices: Optional[Union[List[str], Callable]] = None,
1077
+ title_options: Optional[Union[List[str], Callable]] = None,
1074
1078
  ) -> List[Dict[str, str]]:
1075
1079
  """
1076
1080
  Generates suitable field prompts for a name based on the type of entity (person or business)
@@ -1083,11 +1087,13 @@ class ALIndividual(Individual):
1083
1087
  Default is True.
1084
1088
  show_title: (bool, optional): Determines if the name's title (e.g., Mr., Ms.) should be included in the prompts.
1085
1089
  Default is False.
1086
- title_options (List[str], optional): A list of title options to use in the prompts. Default is defined as a list
1087
- of common titles in English-speaking countries.
1090
+ title_choices (Union[List[str], Callable], optional): A list or callable of title options to use in the prompts. Default is defined as a list
1091
+ of common titles in English-speaking countries, or overridden by value of global `al_name_titles`.
1088
1092
  show_if (Union[str, Dict[str, str], None], optional): Condition to determine which fields to show.
1089
1093
  It can be a string, a dictionary with conditions, or None. Default is None.
1090
1094
  maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
1095
+ 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
+ title_options: (Union[List[str], Callable], optional): Deprecated parameter, use `title_choices` instead. If provided, it will be used to set the title choices.
1091
1097
 
1092
1098
  Returns:
1093
1099
  List[Dict[str, str]]: A list of dictionaries where each dictionary contains field prompt details.
@@ -1096,29 +1102,23 @@ class ALIndividual(Individual):
1096
1102
  If `person_or_business` is set to None, the method will offer the end user a choice
1097
1103
  and will set appropriate "show ifs" conditions for each type.
1098
1104
  """
1099
- if not title_options:
1100
- title_options = [
1101
- "Mr.",
1102
- "Mrs.",
1103
- "Miss",
1104
- "Ms.",
1105
- "Mx.",
1106
- "Dr.",
1107
- "Prof.",
1108
- "Hon.",
1109
- "Rev.",
1110
- "Sir",
1111
- "Lord",
1112
- "Lady",
1113
- "Dame",
1114
- "Maj.",
1115
- "Gen.",
1116
- "Capt.",
1117
- "Lt.",
1118
- "Sgt.",
1119
- "Fr.",
1120
- "Sr.",
1121
- ]
1105
+ if title_options:
1106
+ log(
1107
+ "title_options parameter to name_fields() is deprecated: use title_choices instead"
1108
+ )
1109
+ title_choices = title_options
1110
+
1111
+ if not suffix_choices:
1112
+ suffix_choices = value("al_name_suffixes")
1113
+ if callable(suffix_choices):
1114
+ suffix_choices = suffix_choices()
1115
+
1116
+ if not title_choices:
1117
+ title_choices = value("al_name_titles")
1118
+
1119
+ if callable(title_choices):
1120
+ title_choices = title_choices()
1121
+
1122
1122
  if person_or_business == "person":
1123
1123
  fields = [
1124
1124
  {
@@ -1140,7 +1140,7 @@ class ALIndividual(Individual):
1140
1140
  {
1141
1141
  "label": str(self.suffix_label),
1142
1142
  "field": self.attr_name("name.suffix"),
1143
- "choices": name_suffix(),
1143
+ "choices": suffix_choices,
1144
1144
  "required": False,
1145
1145
  }
1146
1146
  )
@@ -1150,7 +1150,7 @@ class ALIndividual(Individual):
1150
1150
  {
1151
1151
  "label": str(self.name_title_label),
1152
1152
  "field": self.attr_name("name.title"),
1153
- "choices": title_options,
1153
+ "choices": title_choices,
1154
1154
  "required": False,
1155
1155
  },
1156
1156
  )
@@ -1159,8 +1159,6 @@ class ALIndividual(Individual):
1159
1159
  field["show if"] = show_if
1160
1160
  return fields
1161
1161
  elif person_or_business == "business":
1162
- # Note: we don't make use of the name.text field for simplicity
1163
- # TODO: this could be reconsidered`, but name.text tends to lead to developer error
1164
1162
  fields = [
1165
1163
  {
1166
1164
  "label": str(self.business_name_label),
@@ -1218,7 +1216,7 @@ class ALIndividual(Individual):
1218
1216
  {
1219
1217
  "label": str(self.suffix_label),
1220
1218
  "field": self.attr_name("name.suffix"),
1221
- "choices": name_suffix(),
1219
+ "choices": suffix_choices,
1222
1220
  "required": False,
1223
1221
  "show if": show_if_indiv,
1224
1222
  }
@@ -1286,6 +1284,7 @@ class ALIndividual(Individual):
1286
1284
  show_help=False,
1287
1285
  show_if: Union[str, Dict[str, str], None] = None,
1288
1286
  maxlengths: Optional[Dict[str, int]] = None,
1287
+ choices: Optional[Union[List[Dict[str, str]], Callable]] = None,
1289
1288
  ) -> List[Dict[str, str]]:
1290
1289
  """
1291
1290
  Generate fields for capturing gender information, including a
@@ -1295,18 +1294,26 @@ class ALIndividual(Individual):
1295
1294
  show_help (bool): Whether to show additional help text. Defaults to False.
1296
1295
  show_if (Union[str, Dict[str, str], None]): Condition to determine if the field should be shown. Defaults to None.
1297
1296
  maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
1297
+ 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.
1298
1298
 
1299
1299
  Returns:
1300
1300
  List[Dict[str, str]]: A list of dictionaries with field prompts for gender.
1301
+
1302
+ Note:
1303
+ self-described will provide an input that overrides the value of `gender` and is not persisted.
1301
1304
  """
1302
- choices = [
1303
- {str(self.gender_female_label): "female"},
1304
- {str(self.gender_male_label): "male"},
1305
- {str(self.gender_nonbinary_label): "nonbinary"},
1306
- {str(self.gender_prefer_not_to_say_label): "prefer-not-to-say"},
1307
- {str(self.gender_prefer_self_described_label): "self-described"},
1308
- {str(self.gender_unknown_label): "unknown"},
1309
- ]
1305
+ if not choices:
1306
+ choices = [
1307
+ {str(self.gender_female_label): "female"},
1308
+ {str(self.gender_male_label): "male"},
1309
+ {str(self.gender_nonbinary_label): "nonbinary"},
1310
+ {str(self.gender_prefer_not_to_say_label): "prefer-not-to-say"},
1311
+ {str(self.gender_prefer_self_described_label): "self-described"},
1312
+ {str(self.gender_unknown_label): "unknown"},
1313
+ ]
1314
+ if callable(choices):
1315
+ choices = choices()
1316
+
1310
1317
  self_described_input = {
1311
1318
  "label": str(self.gender_self_described_label),
1312
1319
  "field": self.attr_name("gender"),
@@ -1341,6 +1348,7 @@ class ALIndividual(Individual):
1341
1348
  shuffle: bool = False,
1342
1349
  show_unknown: Optional[Union[Literal["guess"], bool]] = "guess",
1343
1350
  maxlengths: Optional[Dict[str, int]] = None,
1351
+ choices: Optional[List[Dict[str, str]]] = None,
1344
1352
  ) -> List[Dict[str, str]]:
1345
1353
  """
1346
1354
  Generate fields for capturing pronoun information.
@@ -1352,16 +1360,15 @@ class ALIndividual(Individual):
1352
1360
  shuffle (bool): Whether to shuffle the order of pronouns. Defaults to False.
1353
1361
  show_unknown (Union[Literal["guess"], bool]): Whether to show an "unknown" option. Can be "guess", True, or False. Defaults to "guess".
1354
1362
  maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
1363
+ choices (Optional[List[Dict[str, str]]]): A list of custom pronoun choices. Defaults to None. If not provided, global magic variable `al_pronoun_choices` will be used.
1355
1364
 
1356
1365
  Returns:
1357
1366
  List[Dict[str, str]]: A list of dictionaries with field prompts for pronouns.
1358
1367
  """
1359
- shuffled_choices = [
1360
- {str(self.pronoun_she_label): "she/her/hers"},
1361
- {str(self.pronoun_he_label): "he/him/his"},
1362
- {str(self.pronoun_they_label): "they/them/theirs"},
1363
- {str(self.pronoun_zir_label): "ze/zir/zirs"},
1364
- ]
1368
+ if choices:
1369
+ shuffled_choices = choices
1370
+ else:
1371
+ shuffled_choices = value("al_pronoun_choices")
1365
1372
  if shuffle:
1366
1373
  random.shuffle(shuffled_choices)
1367
1374
  final_choices = [
@@ -1432,7 +1439,7 @@ class ALIndividual(Individual):
1432
1439
 
1433
1440
  def language_fields(
1434
1441
  self,
1435
- choices: Optional[List[Dict[str, str]]] = None,
1442
+ choices: Optional[Union[List[Dict[str, str]], Callable]] = None,
1436
1443
  style: str = "radio",
1437
1444
  show_if: Union[str, Dict[str, str], None] = None,
1438
1445
  maxlengths: Optional[Dict[str, int]] = None,
@@ -1441,7 +1448,7 @@ class ALIndividual(Individual):
1441
1448
  Generate fields for capturing language preferences.
1442
1449
 
1443
1450
  Args:
1444
- choices (Optional[List[Dict[str, str]]]): A list of language choices. Defaults to None.
1451
+ choices (Optional[Union[List[Dict[str, str]], Callable]]): A list or callable of language choices. Defaults to None.
1445
1452
  style (str): The display style of choices. Defaults to "radio".
1446
1453
  show_if (Union[str, Dict[str, str], None]): Condition to determine if the field should be shown. Defaults to None.
1447
1454
  maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
@@ -1455,6 +1462,8 @@ class ALIndividual(Individual):
1455
1462
  {"Spanish": "es"},
1456
1463
  {"Other": "other"},
1457
1464
  ]
1465
+ if callable(choices):
1466
+ choices = choices()
1458
1467
  other = {
1459
1468
  "label": str(self.language_other_label),
1460
1469
  "field": self.attr_name("language_other"),