CVE-2024-27291 (published 2024-03-21T02:52Z)

Docassemble is an expert system for guided interviews and document assembly. Prior to 1.4.97, it is possible to create a URL that acts as an open redirect. The vulnerability has been patched in version 1.4.97 of the master branch.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/jhpyle/docassemble
commit_details: repo_url: https://github.com/jhpyle/docassemble
author: Jonathan Pyle
author_date: 2024-02-29 12:43:07-05:00
msg: fixed security issues; added pdftk option; added development site is protected Configuration directive; altered error message display so that end users will not see certain types of error messages, but they will be displayed in the log
repository: repo_url: https://github.com/jhpyle/docassemble
repo_name: jhpyle/docassemble
description: A free, open-source expert system for guided interviews and document assembly, based on Python, YAML, and Markdown.
repo_language: Python
owner: jhpyle
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,32 @@
 # Change Log
 
+## [1.4.97] - 2024-02-29
+
+### Added
+- The `pdftk` option under `attachment` and `features` for filling in
+  `pdf template file` attachments using pdftk instead of pikepdf.
+### Changed
+- During the Docker image build process, `pandoc` will run once, so
+  that the first user to assemble a document with `pandoc` will not
+  experience slowness due to LaTeX needing to generate files.
+- Appearance streams will be generated when using `pdf template file`.
+- Error messages related to problems in the source code will no longer
+  be displayed to the user unless the user is an administrator or
+  developer. If you want these error messages to appear to all users,
+  set `debug: True` and `development site is protected: True` in the
+  Configuration. The error messages will be available in
+  `docassemble.log`.
+### Fixed
+- Fixed security issue identified by Riyush Ghimire, affecting
+  versions 1.4.53 to 1.4.96, that could cause contents of files in the
+  filesystem to be revealed. This is a high severity issue and
+  upgrading as soon as possible is recommended.
+- Fixed security issue identified by Riyush Ghimire, affecting
+  versions up to 1.4.96, that allowed an open redirect URL to be formed.
+- Fixed security issue identified by Riyush Ghimire, affecting
+  versions up to 1.4.96, that would allow HTML or JavaScript
+  injection.
+
 ## [1.4.96] - 2024-02-14
 
 ### Fixed

programming_language: Markdown
filename: Dockerfile
change_type: ModificationType.MODIFY
diff: @@ -122,7 +122,13 @@ RUN bash -c \
 && python /tmp/docassemble/Docker/nltkdownload.py \
 && cd /var/www/nltk_data/corpora \
 && unzip -o wordnet.zip \
-&& unzip -o omw-1.4.zip"
+&& unzip -o omw-1.4.zip \
+&& cd /tmp \
+&& mkdir -p /tmp/conv \
+&& pandoc --pdf-engine=lualatex -M latextmpdir=./conv -M pdfa=false /usr/share/docassemble/local3.10/lib/python3.10/site-packages/docassemble/base/data/templates/Legal-Template.yml --template=/usr/share/docassemble/local3.10/lib/python3.10/site-packages/docassemble/base/data/templates/Legal-Template.tex --from=markdown+raw_tex-latex_macros -s -o /tmp/temp.pdf /usr/share/docassemble/local3.10/lib/python3.10/site-packages/docassemble/base/data/templates/hello.md \
+&& rm /tmp/temp.pdf \
+&& pandoc --pdf-engine=lualatex -M latextmpdir=./conv -M pdfa=false --template=/usr/share/docassemble/local3.10/lib/python3.10/site-packages/docassemble/base/data/templates/Legal-Template.rtf -s -o /tmp/temp.rtf /usr/share/docassemble/local3.10/lib/python3.10/site-packages/docassemble/base/data/templates/hello.md \
+&& rm /tmp/temp.rtf"
 
 USER root
 RUN rm -rf /tmp/docassemble

programming_language: Shell
filename: base-words.yml
change_type: ModificationType.MODIFY
diff: @@ -311,6 +311,7 @@
 "Falkland Islands (Malvinas)": Null
 "false": Null
 "Faroe Islands": Null
+"Field cannot contain HTML": Null
 "Fiji": Null
 "File could not be converted: ": Null
 "File deleted.": Null
@@ -1031,6 +1032,7 @@
 "There is already a username and password on this system with the e-mail address": Null
 "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.": Null
 "There was an error.": Null
+"There was an error. Please contact the system administrator.": Null
 "There was an error updating the packages.": Null
 "There was an error with the synchronization.": Null
 "There was a problem connecting to GitHub. Please check your GitHub configuration and try again.": Null

programming_language: CoffeeScript
filename: error.py
change_type: ModificationType.MODIFY
diff: @@ -27,6 +27,9 @@ class DAError(Exception):
         return str(self.value)
 
 
+class DASourceError(DAError):
+    pass
+
 class DANotFoundError(Exception):
     pass
 

programming_language: Python
filename: pandoc.py
change_type: ModificationType.MODIFY
diff: @@ -10,6 +10,9 @@ import time
 import random
 import mimetypes
 import urllib.request
+import convertapi
+import requests
+from pikepdf import Pdf
 import docassemble.base.filter
 import docassemble.base.functions
 from docassemble.base.config import daconfig
@@ -17,9 +20,6 @@ from docassemble.base.logger import logmessage
 from docassemble.base.pdfa import pdf_to_pdfa
 from docassemble.base.pdftk import pdf_encrypt
 from docassemble.base.error import DAError, DAException
-import convertapi
-import requests
-from pikepdf import Pdf
 
 style_find = re.compile(r'{\s*(\\s([1-9])[^\}]+)\\sbasedon[^\}]+heading ([0-9])', flags=re.DOTALL)
 PANDOC_PATH = daconfig.get('pandoc', 'pandoc')
@@ -802,13 +802,31 @@ def concatenate_files(path_list, pdfa=False, password=None, owner_password=None)
             new_path_list.append(path)
     if len(new_path_list) == 0:
         raise DAError("concatenate_files: no valid files to concatenate")
+
     if len(new_path_list) == 1:
         shutil.copyfile(new_path_list[0], pdf_file.name)
     else:
         with Pdf.open(new_path_list[0]) as original:
+            need_appearances = False
+            try:
+                if original.Root.AcroForm.NeedAppearances:
+                    need_appearances = True
+            except:
+                pass
             for additional_file in new_path_list[1:]:
                 with Pdf.open(additional_file) as additional_pdf:
+                    if need_appearances is False:
+                        try:
+                            if additional_pdf.Root.AcroForm.NeedAppearances:
+                                need_appearances = True
+                        except:
+                            pass
                     original.pages.extend(additional_pdf.pages)
+            if need_appearances:
+                try:
+                    original.Root.AcroForm.NeedAppearances = True
+                except:
+                    logmessage("concatenate_files: an additional file had an AcroForm with NeedAppearances but setting NeedAppearances on the final document resulted in an error")
             original.save(pdf_file.name)
     if pdfa:
         pdf_to_pdfa(pdf_file.name)

programming_language: Python
filename: parse.py
change_type: ModificationType.MODIFY
diff: @@ -49,7 +49,7 @@ from docassemble.base import __version__ as da_version
 import docassemble.base.filter
 import docassemble.base.pdftk
 import docassemble.base.file_docx
-from docassemble.base.error import DAError, DANotFoundError, MandatoryQuestion, DAErrorNoEndpoint, DAErrorMissingVariable, ForcedNameError, QuestionError, ResponseError, BackgroundResponseError, BackgroundResponseActionError, CommandError, CodeExecute, DAValidationError, ForcedReRun, LazyNameError, DAAttributeError, DAIndexError, DAException, DANameError
+from docassemble.base.error import DAError, DANotFoundError, MandatoryQuestion, DAErrorNoEndpoint, DAErrorMissingVariable, ForcedNameError, QuestionError, ResponseError, BackgroundResponseError, BackgroundResponseActionError, CommandError, CodeExecute, DAValidationError, ForcedReRun, LazyNameError, DAAttributeError, DAIndexError, DAException, DANameError, DASourceError
 import docassemble.base.functions
 import docassemble.base.util
 from docassemble.base.functions import pickleable_objects, word, get_language, RawValue, get_config
@@ -2063,9 +2063,9 @@ class Question:
 
     def __init__(self, orig_data, caller, **kwargs):
         if not isinstance(orig_data, dict):
-            raise DAError("A block must be in the form of a dictionary." + self.idebug(orig_data))
+            raise DASourceError("A block must be in the form of a dictionary." + self.idebug(orig_data))
         if '__error__' in orig_data:
-            raise DAError(orig_data['__error__'])
+            raise DASourceError(orig_data['__error__'])
         data = {}
         for key, value in orig_data.items():
             data[key.lower()] = value
@@ -2133,9 +2133,9 @@ class Question:
             if directive in data:
                 num_directives += 1
         if num_directives > 1:
-            raise DAError("There can only be one directive in a question.  You had more than one.\nThe directives are yesno, noyes, yesnomaybe, noyesmaybe, fields, buttons, choices, dropdown, combobox, signature, and review" + self.idebug(data))
+            raise DASourceError("There can only be one directive in a question.  You had more than one.\nThe directives are yesno, noyes, yesnomaybe, noyesmaybe, fields, buttons, choices, dropdown, combobox, signature, and review" + self.idebug(data))
         if num_directives > 0 and 'question' not in data:
-            raise DAError("This block is missing a 'question' directive." + self.idebug(data))
+            raise DASourceError("This block is missing a 'question' directive." + self.idebug(data))
         if self.interview.debug:
             for key in data:
                 if key not in ('features', 'scan for variables', 'only sets', 'question', 'code', 'event', 'translations', 'default language', 'on change', 'sections', 'progressive', 'auto open', 'section', 'machine learning storage', 'language', 'prevent going back', 'back button', 'usedefs', 'continue button label', 'continue button color', 'resume button label', 'resume button color', 'back button label', 'corner back button label', 'skip undefined', 'list collect', 'mandatory', 'attachment options', 'script', 'css', 'initial', 'default role', 'command', 'objects from file', 'use objects', 'data', 'variable name', 'data from code', 'objects', 'id', 'ga id', 'segment id', 'segment', 'supersedes', 'order', 'image sets', 'images', 'def', 'mako', 'interview help', 'default screen parts', 'default validation messages', 'generic object', 'generic list object', 'comment', 'metadata', 'modules', 'reset', 'imports', 'terms', 'auto terms', 'role', 'include', 'action buttons', 'if', 'validation code', 'require', 'orelse', 'attachment', 'attachments', 'attachment code', 'attachments code', 'allow emailing', 'allow downloading', 'email subject', 'email body', 'email template', 'email address default', 'progress', 'zip filename', 'action', 'backgroundresponse', 'response', 'binaryresponse', 'all_variables', 'response filename', 'content type', 'redirect url', 'null response', 'sleep', 'include_internal', 'css class', 'table css class', 'response code', 'subquestion', 'reload', 'help', 'audio', 'video', 'decoration', 'signature', 'under', 'pre', 'post', 'right', 'check in', 'yesno', 'noyes', 'yesnomaybe', 'noyesmaybe', 'sets', 'event', 'choices', 'buttons', 'dropdown', 'combobox', 'field', 'shuffle', 'review', 'need', 'depends on', 'target', 'table', 'rows', 'columns', 'require gathered', 'allow reordering', 'edit', 'delete buttons', 'confirm', 'read only', 'edit header', 'confirm', 'show if empty', 'template', 'content file', 'content', 'subject', 'reconsider', 'undefine', 'continue button field', 'fields', 'indent', 'url', 'default', 'datatype', 'extras', 'allowed to set', 'show incomplete', 'not available label', 'required', 'always include editable files', 'question metadata', 'include attachment notice', 'include download tab', 'manual attachment list', 'breadcrumb', 'tabular', 'hide continue button', 'disable continue button', 'pen color', 'gathered'):
@@ -2143,12 +2143,12 @@ class Question:
         if 'features' in data:
             should_append = False
             if not isinstance(data['features'], dict):
-                raise DAError("A features section must be a dictionary." + self.idebug(data))
+                raise DASourceError("A features section must be a dictionary." + self.idebug(data))
             if 'use catchall' in data['features'] and isinstance(data['features']['use catchall'], bool):
                 self.interview.options['use catchall'] = data['features']['use catchall']
             if 'table width' in data['features']:
                 if not isinstance(data['features']['table width'], int):
-                    raise DAError("Table width in features must be an integer." + self.idebug(data))
+                    raise DASourceError("Table width in features must be an integer." + self.idebug(data))
                 self.interview.table_width = data['features']['table width']
             if 'progress bar' in data['features'] and isinstance(data['features']['progress bar'], bool):
                 self.interview.use_progress_bar = data['features']['progress bar']
@@ -2160,7 +2160,7 @@ class Question:
                 self.interview.progress_bar_method = data['features']['progress bar method']
             if 'progress bar multiplier' in data['features'] and isinstance(data['features']['progress bar multiplier'], (int, float)):
                 if data['features']['progress bar multiplier'] <= 0.0 or data['features']['progress bar multiplier'] >= 1.0:
-                    raise DAError("progress bar multiplier in features must be between 0 and 1." + self.idebug(data))
+                    raise DASourceError("progress bar multiplier in features must be between 0 and 1." + self.idebug(data))
                 self.interview.progress_bar_method = data['features']['progress bar multiplier']
             if 'question back button' in data['features'] and isinstance(data['features']['question back button'], bool):
                 self.interview.question_back_button = data['features']['question back button']
@@ -2195,6 +2195,8 @@ class Question:
                 self.interview.recursion_limit = data['features']['recursion limit']
             if 'pdf/a' in data['features'] and data['features']['pdf/a'] in (True, False):
                 self.interview.use_pdf_a = data['features']['pdf/a']
+            if 'pdftk' in data['features'] and data['features']['pdftk'] in (True, False):
+                self.interview.options['use pdftk'] = data['features']['pdftk']
             if 'tagged pdf' in data['features'] and data['features']['tagged pdf'] in (True, False):
                 self.interview.use_tagged_pdf = data['features']['tagged pdf']
             if 'bootstrap theme' in data['features'] and data['features']['bootstrap theme']:
@@ -2231,9 +2233,9 @@ class Question:
                             self.interview.custom_data_types.add(item)
             if 'checkin interval' in data['features']:
                 if not isinstance(data['features']['checkin interval'], int):
-                    raise DAError("A features section checkin interval entry must be an integer." + self.idebug(data))
+                    raise DASourceError("A features section checkin interval entry must be an integer." + self.idebug(data))
                 if data['features']['checkin interval'] > 0 and data['features']['checkin interval'] < 1000:
-                    raise DAError("A features section checkin interval entry must be at least 1000, if not 0." + self.idebug(data))
+                    raise DASourceError("A features section checkin interval entry must be at least 1000, if not 0." + self.idebug(data))
                 self.interview.options['checkin interval'] = data['features']['checkin interval']
             if 'hide corner interface' in data['features']:
                 self.interview.options['hide corner interface'] = data['features']['hide corner interface']
@@ -2242,7 +2244,7 @@ class Question:
                     if isinstance(data['features'][key], list):
                         the_list = data['features'][key]
                     elif isinstance(data['features'][key], dict):
-                        raise DAError("A features section " + key + " entry must be a list or plain text." + self.idebug(data))
+                        raise DASourceError("A features section " + key + " entry must be a list or plain text." + self.idebug(data))
                     else:
                         the_list = [data['features'][key]]
                     for the_file in the_list:
@@ -2252,11 +2254,11 @@ class Question:
             for key in ('default date min', 'default date max'):
                 if key in data['features']:
                     if not isinstance(data['features'][key], str):
-                        raise DAError("A features section " + key + " entry must be plain text." + self.idebug(data))
+                        raise DASourceError("A features section " + key + " entry must be plain text." + self.idebug(data))
                     try:
                         self.interview.options[key] = dateutil.parser.parse(data['features'][key]).astimezone(zoneinfo.ZoneInfo(docassemble.base.functions.get_default_timezone()))
                     except:
-                        raise DAError("The " + key + " in features did not contain a valid date." + self.idebug(data))
+                        raise DASourceError("The " + key + " in features did not contain a valid date." + self.idebug(data))
         if 'field' in data and not ('yesno' in data or 'noyes' in data or 'yesnomaybe' in data or 'noyesmaybe' in data or 'buttons' in data or 'choices' in data or 'dropdown' in data or 'combobox' in data):
             data['continue button field'] = data['field']
             del data['field']
@@ -2272,23 +2274,23 @@ class Question:
                 for key in data['only sets']:
                     self.fields_used.add(key)
             else:
-                raise DAError("An only sets phrase must be text or a list." + self.idebug(data))
+                raise DASourceError("An only sets phrase must be text or a list." + self.idebug(data))
             self.scan_for_variables = False
         if 'question' in data and 'code' in data:
-            raise DAError("A block can be a question block or a code block but cannot be both at the same time." + self.idebug(data))
+            raise DASourceError("A block can be a question block or a code block but cannot be both at the same time." + self.idebug(data))
         if 'event' in data:
             if 'field' in data or 'fields' in data or 'yesno' in data or 'noyes' in data:
-                raise DAError("The 'event' designator is for special screens that do not gather information and can only be used with 'buttons' or with no other controls." + self.idebug(data))
+                raise DASourceError("The 'event' designator is for special screens that do not gather information and can only be used with 'buttons' or with no other controls." + self.idebug(data))
         if 'translations' in data:
             should_append = False
             if not isinstance(data['translations'], list):
-                raise DAError("A 'translations' block must be a list" + self.idebug(data))
+                raise DASourceError("A 'translations' block must be a list" + self.idebug(data))
             tr_todo = []
             for item in data['translations']:
                 if not isinstance(item, str):
-                    raise DAError("A 'translations' block must be a list of text items" + self.idebug(data))
+                    raise DASourceError("A 'translations' block must be a list of text items" + self.idebug(data))
                 if not (item.endswith('.xlsx') or item.endswith('.xlf') or item.endswith('.xliff')):
-                    raise DAError("Invalid translations entry '" + item + "'.  A translations entry must refer to a file ending in .xlsx, .xlf, or .xliff." + self.idebug(data))
+                    raise DASourceError("Invalid translations entry '" + item + "'.  A translations entry must refer to a file ending in .xlsx, .xlf, or .xliff." + self.idebug(data))
                 parts = item.split(":")
                 if len(parts) == 1:
                     item = re.sub(r'^data/sources/', '', item)
@@ -2299,7 +2301,7 @@ class Question:
                 elif len(parts) == 2 and parts[0].startswith('docassemble.') and parts[1].startswith('data/sources/'):
                     tr_todo.append(item)
                 else:
-                    raise DAError("Invalid translations entry: " + item + ".  A translations entry must refer to a data sources file" + self.idebug(data))
+                    raise DASourceError("Invalid translations entry: " + item + ".  A translations entry must refer to a data sources file" + self.idebug(data))
             for item in tr_todo:
                 self.interview.translations.append(item)
                 if item.endswith(".xlsx"):
@@ -2387,12 +2389,12 @@ class Question:
             should_append = False
             self.scan_for_variables = False
             if not isinstance(data['on change'], dict):
-                raise DAError("An on change block must be a dictionary." + self.idebug(data))
+                raise DASourceError("An on change block must be a dictionary." + self.idebug(data))
             if len(data) > 1:
-                raise DAError("An on change block must not contain any other keys." + self.idebug(data))
+                raise DASourceError("An on change block must not contain any other keys." + self.idebug(data))
             for key, val in data['on change'].items():
                 if not (isinstance(key, str) and isinstance(val, str)):
-                    raise DAError("An on change block must be a dictionary where the keys are field names and the values are Python code." + self.idebug(data))
+                    raise DASourceError("An on change block must be a dictionary where the keys are field names and the values are Python code." + self.idebug(data))
                 if key not in self.interview.onchange:
                     self.interview.onchange[key] = []
                 self.interview.onchange[key].append(compile(val, '<on change code>', 'exec'))
@@ -2400,26 +2402,26 @@ class Question:
         if 'sections' in data:
             should_append = False
             if not isinstance(data['sections'], list):
-                raise DAError("A sections list must be a list." + self.idebug(data))
+                raise DASourceError("A sections list must be a list." + self.idebug(data))
             the_language = data.get('language', '*')
             self.interview.sections[the_language] = data['sections']
         if 'progressive' in data:
             if 'sections' not in data:
-                raise DAError("A progressive directive can only be used with sections." + self.idebug(data))
+                raise DASourceError("A progressive directive can only be used with sections." + self.idebug(data))
             if not isinstance(data['progressive'], bool):
-                raise DAError("A progressive directive can only be true or false." + self.idebug(data))
+                raise DASourceError("A progressive directive can only be true or false." + self.idebug(data))
             self.interview.sections_progressive = data['progressive']
         if 'auto open' in data:
             if 'sections' not in data:
-                raise DAError("An auto open directive can only be used with sections." + self.idebug(data))
+                raise DASourceError("An auto open directive can only be used with sections." + self.idebug(data))
             if not isinstance(data['auto open'], bool):
-                raise DAError("An auto open directive can only be true or false." + self.idebug(data))
+                raise DASourceError("An auto open directive can only be true or false." + self.idebug(data))
             self.interview.sections_auto_open = data['auto open']
         if 'machine learning storage' in data:
             should_append = False
             new_storage = data['machine learning storage']
             if not new_storage.endswith('.json'):
-                raise DAError("Invalid machine learning storage entry '" + str(data['machine learning storage']) + ".'  A machine learning storage entry must refer to a file ending in .json." + self.idebug(data))
+                raise DASourceError("Invalid machine learning storage entry '" + str(data['machine learning storage']) + ".'  A machine learning storage entry must refer to a file ending in .json." + self.idebug(data))
             parts = new_storage.split(":")
             if len(parts) == 1:
                 new_storage = re.sub(r'^data/sources/', '', new_storage)
@@ -2430,7 +2432,7 @@ class Question:
             elif len(parts) == 2 and parts[0].startswith('docassemble.') and parts[1].startswith('data/sources/'):
                 self.interview.set_ml_store(data['machine learning storage'])
             else:
-                raise DAError("Invalid machine learning storage entry: " + str(data['machine learning storage']) + self.idebug(data))
+                raise DASourceError("Invalid machine learning storage entry: " + str(data['machine learning storage']) + self.idebug(data))
         if 'language' in data:
             self.language = data['language']
         else:
@@ -2448,13 +2450,13 @@ class Question:
             if isinstance(data['allowed to set'], list):
                 for item in data['allowed to set']:
                     if not isinstance(item, str):
-                        raise DAError("When allowed to set is a list, it must be a list of text items." + self.idebug(data))
+                        raise DASourceError("When allowed to set is a list, it must be a list of text items." + self.idebug(data))
                 self.allowed_to_set = data['allowed to set']
             elif isinstance(data['allowed to set'], str):
                 self.allowed_to_set = compile(data['allowed to set'], '<allowed to set>', 'eval')
                 self.find_fields_in(data['allowed to set'])
             else:
-                raise DAError("When allowed to set is not a list, it must be plain text." + self.idebug(data))
+                raise DASourceError("When allowed to set is not a list, it must be plain text." + self.idebug(data))
         if 'hide continue button' in data and 'question' in data:
             self.hide_continue_button = compile(data['hide continue button'], '<hide continue button>', 'eval')
             self.find_fields_in(data['hide continue button'])
@@ -2469,24 +2471,24 @@ class Question:
                 usedefs = [data['usedefs']]
             for usedef in usedefs:
                 if isinstance(usedef, (dict, list, set, bool)):
-                    raise DAError("A usedefs section must consist of a list of strings or a single string." + self.idebug(data))
+                    raise DASourceError("A usedefs section must consist of a list of strings or a single string." + self.idebug(data))
                 if usedef not in self.interview.defs:
-                    raise DAError('Referred to a non-existent def "' + usedef + '."  All defs must be defined before they are used.' + self.idebug(data))
+                    raise DASourceError('Referred to a non-existent def "' + usedef + '."  All defs must be defined before they are used.' + self.idebug(data))
                 defs.extend(self.interview.defs[usedef])
             definitions = "\n".join(defs) + "\n"
         else:
             definitions = ""
         if 'section' in data:
             if 'question' not in data:
-                raise DAError("You can only set the section from a question." + self.idebug(data))
+                raise DASourceError("You can only set the section from a question." + self.idebug(data))
             self.section = TextObject(definitions + str(data['section']), question=self)
         if 'continue button label' in data:
             if 'yesno' in data or 'noyes' in data or 'yesnomaybe' in data or 'noyesmaybe' in data or 'buttons' in data:
-                raise DAError("You cannot set a continue button label if the type of question is yesno, noyes, yesnomaybe, noyesmaybe, or buttons." + self.idebug(data))
+                raise DASourceError("You cannot set a continue button label if the type of question is yesno, noyes, yesnomaybe, noyesmaybe, or buttons." + self.idebug(data))
             self.continuelabel = TextObject(definitions + str(data['continue button label']), question=self)
         if 'resume button label' in data:
             if 'review' not in data:
-                raise DAError("You cannot set a resume button label if the type of question is not review." + self.idebug(data))
+                raise DASourceError("You cannot set a resume button label if the type of question is not review." + self.idebug(data))
             self.continuelabel = TextObject(definitions + str(data['resume button label']), question=self)
         if 'continue button color' in data:
             self.continuecolor = TextObject(definitions + str(data['continue button color']), question=self)
@@ -2498,12 +2500,12 @@ class Question:
             self.cornerbackbuttonlabel = TextObject(definitions + str(data['corner back button label']), question=self)
         if 'skip undefined' in data:
             if 'review' not in data:
-                raise DAError("You cannot set the skip undefined directive if the type of question is not review." + self.idebug(data))
+                raise DASourceError("You cannot set the skip undefined directive if the type of question is not review." + self.idebug(data))
             if not data['skip undefined']:
                 self.skip_undefined = False
         if 'list collect' in data:
             if 'fields' not in data:
-                raise DAError("You cannot set list collect without a fields specifier." + self.idebug(data))
+                raise DASourceError("You cannot set list collect without a fields specifier." + self.idebug(data))
             if isinstance(data['list collect'], (str, bool)):
                 self.list_collect = compile(str(data['list collect']), '<list collect code>', 'eval')
             elif isinstance(data['list collect'], dict):
@@ -2522,14 +2524,14 @@ class Question:
                 if 'add another label' in data['list collect']:
                     self.list_collect_add_another_label = TextObject(definitions + str(data['list collect']['add another label']), question=self)
             else:
-                raise DAError("Invalid data under list collect." + self.idebug(data))
+                raise DASourceError("Invalid data under list collect." + self.idebug(data))
         if 'mandatory' in data:
             if 'initial' in data:
-                raise DAError("You cannot use the mandatory modifier and the initial modifier at the same time." + self.idebug(data))
+                raise DASourceError("You cannot use the mandatory modifier and the initial modifier at the same time." + self.idebug(data))
             if 'id' not in data and self.interview.debug and self.interview.source.package.startswith('docassemble.playground'):
                 self.interview.issue['mandatory_id'] = True
             if 'question' not in data and 'code' not in data and 'objects' not in data and 'attachment' not in data and 'data' not in data and 'data from code' not in data:
-                raise DAError("You cannot use the mandatory modifier on this type of block." + self.idebug(data))
+                raise DASourceError("You cannot use the mandatory modifier on this type of block." + self.idebug(data))
             if data['mandatory'] is True:
                 self.is_mandatory = True
                 self.mandatory_code = None
@@ -2552,7 +2554,7 @@ class Question:
                 data['attachment options'] = [data['attachment options']]
             for attachment_option in data['attachment options']:
                 if not isinstance(attachment_option, dict):
-                    raise DAError("An attachment option must a dictionary." + self.idebug(data))
+                    raise DASourceError("An attachment option must a dictionary." + self.idebug(data))
                 for key in attachment_option:
                     value = attachment_option[key]
                     if key == 'initial yaml':
@@ -2564,7 +2566,7 @@ class Question:
                             the_list = [value]
                         for yaml_file in the_list:
                             if not isinstance(yaml_file, str):
-                                raise DAError('An initial yaml file must be a string.' + self.idebug(data))
+                                raise DASourceError('An initial yaml file must be a string.' + self.idebug(data))
                             self.interview.attachment_options['initial_yaml'].append(FileInPackage(yaml_file, 'template', self.package))
                     elif key == 'additional yaml':
                         if 'additional_yaml' not in self.interview.attachment_options:
@@ -2575,30 +2577,30 @@ class Question:
                             the_list = [value]
                         for yaml_file in the_list:
                             if not isinstance(yaml_file, str):
-                                raise DAError('An additional yaml file must be a string.' + self.idebug(data))
+                                raise DASourceError('An additional yaml file must be a string.' + self.idebug(data))
                             self.interview.attachment_options['additional_yaml'].append(FileInPackage(yaml_file, 'template', self.package))
                     elif key == 'template file':
                         if not isinstance(value, str):
-                            raise DAError('The template file must be a string.' + self.idebug(data))
+                            raise DASourceError('The template file must be a string.' + self.idebug(data))
                         self.interview.attachment_options['template_file'] = FileInPackage(value, 'template', self.package)
                     elif key == 'rtf template file':
                         if not isinstance(value, str):
-                            raise DAError('The rtf template file must be a string.' + self.idebug(data))
+                            raise DASourceError('The rtf template file must be a string.' + self.idebug(data))
                         self.interview.attachment_options['rtf_template_file'] = FileInPackage(value, 'template', self.package)
                     elif key == 'docx reference file':
                         if not isinstance(value, str):
-                            raise DAError('The docx reference file must be a string.' + self.idebug(data))
+                            raise DASourceError('The docx reference file must be a string.' + self.idebug(data))
                         self.interview.attachment_options['docx_reference_file'] = FileInPackage(value, 'template', self.package)
         if 'script' in data:
             if not isinstance(data['script'], str):
-                raise DAError("A script section must be plain text." + self.idebug(data))
+                raise DASourceError("A script section must be plain text." + self.idebug(data))
             self.script = TextObject(definitions + DO_NOT_TRANSLATE + str(data['script']), question=self)
         if 'css' in data:
             if not isinstance(data['css'], str):
-                raise DAError("A css section must be plain text." + self.idebug(data))
+                raise DASourceError("A css section must be plain text." + self.idebug(data))
             self.css = TextObject(definitions + DO_NOT_TRANSLATE + str(data['css']), question=self)
         if 'initial' in data and 'code' not in data:
-            raise DAError("Only a code block can be marked as initial." + self.idebug(data))
+            raise DASourceError("Only a code block can be marked as initial." + self.idebug(data))
         if 'initial' in data or 'default role' in data:
             if 'default role' in data or data['initial'] is True:
                 self.is_initial = True
@@ -2641,10 +2643,10 @@ class Question:
                         else:
                             self.other_fields_used.add(key)
                 else:
-                    raise DAError("An objects section cannot contain a nested list." + self.idebug(data))
+                    raise DASourceError("An objects section cannot contain a nested list." + self.idebug(data))
         if 'data' in data and 'variable name' in data:
             if not isinstance(data['variable name'], str):
-                raise DAError("A data block variable name must be plain text." + self.idebug(data))
+                raise DASourceError("A data block variable name must be plain text." + self.idebug(data))
             if self.scan_for_variables:
                 self.fields_used.add(data['variable name'].strip())
             else:
@@ -2669,7 +2671,7 @@ class Question:
             self.fields.append(Field({'saveas': data['variable name'].strip(), 'type': 'data', 'data': self.recursive_dataobject(data['data'])}))
         if 'data from code' in data and 'variable name' in data:
             if not isinstance(data['variable name'], str):
-                raise DAError("A data from code block variable name must be plain text." + self.idebug(data))
+                raise DASourceError("A data from code block variable name must be plain text." + self.idebug(data))
             if self.scan_for_variables:
                 self.fields_used.add(data['variable name'])
             else:
@@ -2695,7 +2697,7 @@ class Question:
         if 'objects' in data:
             if not isinstance(data['objects'], list):
                 data['objects'] = [data['objects']]
-                # raise DAError("An objects section must be organized as a list." + self.idebug(data))
+                # raise DASourceError("An objects section must be organized as a list." + self.idebug(data))
             self.question_type = 'objects'
             self.objects = data['objects']
             for item in data['objects']:
@@ -2707,10 +2709,10 @@ class Question:
                         else:
                             self.other_fields_used.add(key)
                 else:
-                    raise DAError("An objects section cannot contain a nested list." + self.idebug(data))
+                    raise DASourceError("An objects section cannot contain a nested list." + self.idebug(data))
         if 'id' in data:
             # if str(data['id']) in self.interview.ids_in_use:
-            #     raise DAError("The id " + str(data['id']) + " is already in use by another block.  Id names must be unique." + self.idebug(data))
+            #     raise DASourceError("The id " + str(data['id']) + " is already in use by another block.  Id names must be unique." + self.idebug(data))
             self.id = str(data['id']).strip()
             if self.interview.debug and self.interview.source.package.startswith('docassemble.playground') and self.id in self.interview.ids_in_use:
                 self.interview.issue['id_collision'] = self.id
@@ -2718,31 +2720,31 @@ class Question:
             self.interview.questions_by_id[self.id] = self
         if 'ga id' in data:
             if not isinstance(data['ga id'], str):
-                raise DAError("A 'ga id' must refer to text." + self.idebug(data))
+                raise DASourceError("A 'ga id' must refer to text." + self.idebug(data))
             self.ga_id = TextObject(definitions + str(data['ga id']), question=self)
         if 'segment id' in data:
             if not isinstance(data['segment id'], str):
-                raise DAError("A 'segment id' must refer to text." + self.idebug(data))
+                raise DASourceError("A 'segment id' must refer to text." + self.idebug(data))
             if not hasattr(self, 'segment'):
                 self.segment = {'arguments': {}}
             self.segment['id'] = TextObject(definitions + str(data['segment id']), question=self)
         if 'segment' in data:
             if not isinstance(data['segment'], dict):
-                raise DAError("A 'segment' must refer to a dictionary." + self.idebug(data))
+                raise DASourceError("A 'segment' must refer to a dictionary." + self.idebug(data))
             if 'id' in data['segment']:
                 if not isinstance(data['segment']['id'], str):
-                    raise DAError("An 'id' under 'segment' must refer to text." + self.idebug(data))
+                    raise DASourceError("An 'id' under 'segment' must refer to text." + self.idebug(data))
                 if not hasattr(self, 'segment'):
                     self.segment = {'arguments': {}}
                 self.segment['id'] = TextObject(definitions + str(data['segment']['id']), question=self)
             if 'arguments' in data['segment']:
                 if not isinstance(data['segment']['arguments'], dict):
-                    raise DAError("An 'arguments' under 'segment' must refer to a dictionary." + self.idebug(data))
+                    raise DASourceError("An 'arguments' under 'segment' must refer to a dictionary." + self.idebug(data))
                 if not hasattr(self, 'segment'):
                     self.segment = {'arguments': {}}
                 for key, val in data['segment']['arguments'].items():
                     if not isinstance(val, (str, int, float, bool)):
-                        raise DAError("Each item under 'arguments' in a 'segment' must be plain text." + self.idebug(data))
+                        raise DASourceError("Each item under 'arguments' in a 'segment' must be plain text." + self.idebug(data))
                     self.segment['arguments'][key] = TextObject(definitions + str(val), question=self)
         if 'supersedes' in data:
             if not isinstance(data['supersedes'], list):
@@ -2753,16 +2755,16 @@ class Question:
         if 'order' in data:
             should_append = False
             if 'question' in data or 'code' in data or 'attachment' in data or 'attachments' in data or 'template' in data:
-                raise DAError("An 'order' block cannot be combined with another type of block." + self.idebug(data))
+                raise DASourceError("An 'order' block cannot be combined with another type of block." + self.idebug(data))
             if not isinstance(data['order'], list):
-                raise DAError("An 'order' block must be a list." + self.idebug(data))
+                raise DASourceError("An 'order' block must be a list." + self.idebug(data))
             self.interview.id_orderings.append({'type': "order", 'order': [str(x) for x in data['order']]})
         for key in ('image sets', 'images'):
             if key not in data:
                 continue
             should_append = False
             if not isinstance(data[key], dict):
-                raise DAError("The '" + key + "' section needs to be a dictionary, not a list or text." + self.idebug(data))
+                raise DASourceError("The '" + key + "' section needs to be a dictionary, not a list or text." + self.idebug(data))
             if key == 'images':
                 data[key] = {'unspecified': {'images': data[key]}}
             elif 'images' in data[key] and 'attribution' in data[key]:
@@ -2770,11 +2772,11 @@ class Question:
             for setname, image_set in data[key].items():
                 if not isinstance(image_set, dict):
                     if key == 'image sets':
-                        raise DAError("Each item in the 'image sets' section needs to be a dictionary, not a list.  Each dictionary item should have an 'images' definition (which can be a dictionary or list) and an optional 'attribution' definition (which must be text)." + self.idebug(data))
-                    raise DAError("Each item in the 'images' section needs to be a dictionary, not a list." + self.idebug(data))
+                        raise DASourceError("Each item in the 'image sets' section needs to be a dictionary, not a list.  Each dictionary item should have an 'images' definition (which can be a dictionary or list) and an optional 'attribution' definition (which must be text)." + self.idebug(data))
+                    raise DASourceError("Each item in the 'images' section needs to be a dictionary, not a list." + self.idebug(data))
                 if 'attribution' in image_set:
                     if not isinstance(image_set['attribution'], str):
-                        raise DAError("An attribution in an 'image set' section cannot be a dictionary or a list." + self.idebug(data))
+                        raise DASourceError("An attribution in an 'image set' section cannot be a dictionary or a list." + self.idebug(data))
                     attribution = re.sub(r'\n', ' ', image_set['attribution'].strip())
                 else:
                     attribution = None
@@ -2785,8 +2787,8 @@ class Question:
                         image_list = [image_set['images']]
                     else:
                         if key == 'image set':
-                            raise DAError("An 'images' definition in an 'image set' item must be a dictionary or a list." + self.idebug(data))
-                        raise DAError("An 'images' section must be a dictionary or a list." + self.idebug(data))
+                            raise DASourceError("An 'images' definition in an 'image set' item must be a dictionary or a list." + self.idebug(data))
+                        raise DASourceError("An 'images' section must be a dictionary or a list." + self.idebug(data))
                     for image in image_list:
                         if not isinstance(image, dict):
                             the_image = {str(image): str(image)}
@@ -2797,7 +2799,7 @@ class Question:
         if 'def' in data:
             should_append = False
             if not isinstance(data['def'], str):
-                raise DAError("A def name must be a string." + self.idebug(data))
+                raise DASourceError("A def name must be a string." + self.idebug(data))
             if data['def'] not in self.interview.defs:
                 self.interview.defs[data['def']] = []
             if 'mako' in data:
@@ -2806,15 +2808,15 @@ class Question:
                 elif isinstance(data['mako'], list):
                     list_of_defs = data['mako']
                 else:
-                    raise DAError("A mako template definition must be a string or a list of strings." + self.idebug(data))
+                    raise DASourceError("A mako template definition must be a string or a list of strings." + self.idebug(data))
                 for definition in list_of_defs:
                     if not isinstance(definition, str):
-                        raise DAError("A mako template definition must be a string." + self.idebug(data))
+                        raise DASourceError("A mako template definition must be a string." + self.idebug(data))
                     self.interview.defs[data['def']].append(definition)
         if 'interview help' in data:
             should_append = False
             if isinstance(data['interview help'], list):
-                raise DAError("An interview help section must not be in the form of a list." + self.idebug(data))
+                raise DASourceError("An interview help section must not be in the form of a list." + self.idebug(data))
             if not isinstance(data['interview help'], dict):
                 data['interview help'] = {'content': str(data['interview help'])}
             audiovideo = []
@@ -2828,7 +2830,7 @@ class Question:
                 audiovideo = []
                 for the_item in the_list:
                     if isinstance(the_item, (list, dict)):
-                        raise DAError("An interview help audio section must be in the form of a text item or a list of text items." + self.idebug(data))
+                        raise DASourceError("An interview help audio section must be in the form of a text item or a list of text items." + self.idebug(data))
                     audiovideo.append({'text': TextObject(definitions + str(data['interview help']['audio']), question=self), 'package': self.package, 'type': 'audio'})
             if 'video' in data['interview help']:
                 if not isinstance(data['interview help']['video'], list):
@@ -2837,7 +2839,7 @@ class Question:
                     the_list = data['interview help']['video']
                 for the_item in the_list:
                     if isinstance(the_item, (list, dict)):
-                        raise DAError("An interview help video section must be in the form of a text item or a list of text items." + self.idebug(data))
+                        raise DASourceError("An interview help video section must be in the form of a text item or a list of text items." + self.idebug(data))
                     audiovideo.append({'text': TextObject(definitions + str(data['interview help']['video']), question=self), 'package': self.package, 'type': 'video'})
             if 'video' not in data['interview help'] and 'audio' not in data['interview help']:
                 audiovideo = None
@@ -2845,21 +2847,21 @@ class Question:
                 if not isinstance(data['interview help']['heading'], (dict, list)):
                     help_heading = TextObject(definitions + str(data['interview help']['heading']), question=self)
                 else:
-                    raise DAError("A heading within an interview help section must be text, not a list or a dictionary." + self.idebug(data))
+                    raise DASourceError("A heading within an interview help section must be text, not a list or a dictionary." + self.idebug(data))
             else:
                 help_heading = None
             if 'content' in data['interview help']:
                 if not isinstance(data['interview help']['content'], (dict, list)):
                     help_content = TextObject(definitions + str(data['interview help']['content']), question=self)
                 else:
-                    raise DAError("Help content must be text, not a list or a dictionary." + self.idebug(data))
+                    raise DASourceError("Help content must be text, not a list or a dictionary." + self.idebug(data))
             else:
-                raise DAError("No content section was found in an interview help section." + self.idebug(data))
+                raise DASourceError("No content section was found in an interview help section." + self.idebug(data))
             if 'label' in data['interview help']:
                 if not isinstance(data['interview help']['label'], (dict, list)):
                     help_label = TextObject(definitions + str(data['interview help']['label']), question=self)
                 else:
-                    raise DAError("Help label must be text, not a list or a dictionary." + self.idebug(data))
+                    raise DASourceError("Help label must be text, not a list or a dictionary." + self.idebug(data))
             else:
                 help_label = None
             if self.language not in self.interview.helptext:
@@ -2868,7 +2870,7 @@ class Question:
         if 'default screen parts' in data:
             should_append = False
             if not isinstance(data['default screen parts'], dict):
-                raise DAError("A default screen parts block must be in the form of a dictionary." + self.idebug(data))
+                raise DASourceError("A default screen parts block must be in the form of a dictionary." + self.idebug(data))
             if self.language not in self.interview.default_screen_parts:
                 self.interview.default_screen_parts[self.language] = {}
             for key, content in data['default screen parts'].items():
@@ -2877,17 +2879,17 @@ class Question:
                         del self.interview.default_screen_parts[self.language][key]
                 else:
                     if not (isinstance(key, str) and isinstance(content, str)):
-                        raise DAError("A default screen parts block must be a dictionary of text keys and text values." + self.idebug(data))
+                        raise DASourceError("A default screen parts block must be a dictionary of text keys and text values." + self.idebug(data))
                     self.interview.default_screen_parts[self.language][key] = TextObject(definitions + str(content.strip()), question=self)
         if 'default validation messages' in data:
             should_append = False
             if not isinstance(data['default validation messages'], dict):
-                raise DAError("A default validation messages block must be in the form of a dictionary." + self.idebug(data))
+                raise DASourceError("A default validation messages block must be in the form of a dictionary." + self.idebug(data))
             if self.language not in self.interview.default_validation_messages:
                 self.interview.default_validation_messages[self.language] = {}
             for validation_key, validation_message in data['default validation messages'].items():
                 if not (isinstance(validation_key, str) and isinstance(validation_message, str)):
-                    raise DAError("A validation messages block must be a dictionary of text keys and text values." + self.idebug(data))
+                    raise DASourceError("A validation messages block must be a dictionary of text keys and text values." + self.idebug(data))
                 self.interview.default_validation_messages[self.language][validation_key] = validation_message.strip()
         if 'generic object' in data:
             self.is_generic = True
@@ -2904,7 +2906,7 @@ class Question:
         if 'metadata' in data:
             for key in data:
                 if key not in ('metadata', 'comment'):
-                    raise DAError("A metadata directive cannot be mixed with other directives." + self.idebug(data))
+                    raise DASourceError("A metadata directive cannot be mixed with other directives." + self.idebug(data))
             should_append = False
             if isinstance(data['metadata'], dict):
                 data['metadata']['_origin_path'] = self.from_source.path
@@ -2913,7 +2915,7 @@ class Question:
                     self.interview.default_language = data['metadata']['default language']
                 self.interview.metadata.append(data['metadata'])
             else:
-                raise DAError("A metadata section must be organized as a dictionary." + self.idebug(data))
+                raise DASourceError("A metadata section must be organized as a dictionary." + self.idebug(data))
         if 'modules' in data:
             if isinstance(data['modules'], str):
                 data['modules'] = [data['modules']]
@@ -2926,7 +2928,7 @@ class Question:
                 self.question_type = 'modules'
                 self.module_list = data['modules']
             else:
-                raise DAError("A modules section must be organized as a list." + self.idebug(data))
+                raise DASourceError("A modules section must be organized as a list." + self.idebug(data))
         if 'reset' in data:
             # logmessage("Found a reset")
             if isinstance(data['reset'], str):
@@ -2935,7 +2937,7 @@ class Question:
                 self.question_type = 'reset'
                 self.reset_list = data['reset']
             else:
-                raise DAError("A reset section must be organized as a list." + self.idebug(data))
+                raise DASourceError("A reset section must be organized as a list." + self.idebug(data))
         if 'imports' in data:
             if isinstance(data['imports'], str):
                 data['imports'] = [data['imports']]
@@ -2943,16 +2945,16 @@ class Question:
                 self.question_type = 'imports'
                 self.module_list = data['imports']
             else:
-                raise DAError("An imports section must be organized as a list." + self.idebug(data))
+                raise DASourceError("An imports section must be organized as a list." + self.idebug(data))
         if 'terms' in data and 'question' in data:
             if not isinstance(data['terms'], (dict, list)):
-                raise DAError("Terms must be organized as a dictionary or a list." + self.idebug(data))
+                raise DASourceError("Terms must be organized as a dictionary or a list." + self.idebug(data))
 
             if isinstance(data['terms'], dict):
                 data['terms'] = [data['terms']]
             for termitem in data['terms']:
                 if not isinstance(termitem, dict):
-                    raise DAError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data))
+                    raise DASourceError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data))
                 if len(termitem) == 2 and 'phrases' in termitem and isinstance(termitem['phrases'], list) and 'definition' in termitem:
                     termitems = [(phrase, termitem['definition']) for phrase in termitem['phrases']]
                 else:
@@ -2970,12 +2972,12 @@ class Question:
                     self.terms[lower_term] = {'definition': TextObject(definitions + str(definition), question=self), 're': re_dict, 'alt_terms': alt_terms}
         if 'auto terms' in data and 'question' in data:
             if not isinstance(data['auto terms'], (dict, list)):
-                raise DAError("Terms must be organized as a dictionary or a list." + self.idebug(data))
+                raise DASourceError("Terms must be organized as a dictionary or a list." + self.idebug(data))
             if isinstance(data['auto terms'], dict):
                 data['auto terms'] = [data['auto terms']]
             for termitem in data['auto terms']:
                 if not isinstance(termitem, dict):
-                    raise DAError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data))
+                    raise DASourceError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data))
                 if len(termitem) == 2 and 'phrases' in termitem and isinstance(termitem['phrases'], list) and 'definition' in termitem:
                     termitems = [(phrase, termitem['definition']) for phrase in termitem['phrases']]
                 else:
@@ -3015,7 +3017,7 @@ class Question:
                                         lower_other = re.sub(r'\s+', ' ', tr_tuple[0].lower())
                                         self.interview.terms[lang][tr_tuple[0]] = {'definition': definition_textobject.other_lang[lang][0], 're': re.compile(r"{(?i)(%s)(\|[^\}]*)?}" % (re.sub(r'\s', '\\\s+', lower_other),), re.IGNORECASE | re.DOTALL)}  # noqa: W605
                     else:
-                        raise DAError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data))
+                        raise DASourceError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data))
             elif isinstance(data['terms'], dict):
                 for term in data['terms']:
                     lower_term = re.sub(r'\s+', ' ', term.lower())
@@ -3030,7 +3032,7 @@ class Question:
                                 lower_other = re.sub(r'\s+', ' ', tr_tuple[0].lower())
                                 self.interview.terms[lang][tr_tuple[0]] = {'definition': definition_textobject.other_lang[lang][0], 're': re.compile(r"{(?i)(%s)(\|[^\}]*)?}" % (re.sub(r'\s', '\\\s+', lower_other),), re.IGNORECASE | re.DOTALL)}  # noqa: W605
             else:
-                raise DAError("A terms section must be organized as a dictionary or a list." + self.idebug(data))
+                raise DASourceError("A terms section must be organized as a dictionary or a list." + self.idebug(data))
         if 'auto terms' in data and 'question' not in data:
             should_append = False
             if self.language not in self.interview.autoterms:
@@ -3055,7 +3057,7 @@ class Question:
                                         lower_other = re.sub(r'\s+', ' ', tr_tuple[0].lower())
                                         self.interview.autoterms[lang][tr_tuple[0]] = {'definition': definition_textobject.other_lang[lang][0], 're': re.compile(r"{?(?i)\b(%s)\b}?" % (re.sub(r'\s', '\\\s+', lower_other),), re.IGNORECASE | re.DOTALL)}  # noqa: W605
                     else:
-                        raise DAError("An auto terms section organized as a list must be a list of dictionary items." + self.idebug(data))
+                        raise DASourceError("An auto terms section organized as a list must be a list of dictionary items." + self.idebug(data))
             elif isinstance(data['auto terms'], dict):
                 for term in data['auto terms']:
                     lower_term = re.sub(r'\s+', ' ', term.lower())
@@ -3070,7 +3072,7 @@ class Question:
                                 lower_other = re.sub(r'\s+', ' ', tr_tuple[0].lower())
                                 self.interview.autoterms[lang][tr_tuple[0]] = {'definition': definition_textobject.other_lang[lang][0], 're': re.compile(r"{?(?i)\b(%s)\b}?" % (re.sub(r'\s', '\\\s+', lower_other),), re.IGNORECASE | re.DOTALL)}  # noqa: W605
             else:
-                raise DAError("An auto terms section must be organized as a dictionary or a list." + self.idebug(data))
+                raise DASourceError("An auto terms section must be organized as a dictionary or a list." + self.idebug(data))
         if 'default role' in data:
             if 'code' not in data:
                 should_append = False
@@ -3079,7 +3081,7 @@ class Question:
             elif isinstance(data['default role'], list):
                 self.interview.default_role = data['default role']
             else:
-                raise DAError("A default role must be a list or a string." + self.idebug(data))
+                raise DASourceError("A default role must be a list or a string." + self.idebug(data))
         if 'role' in data:
             if isinstance(data['role'], str):
                 if data['role'] not in self.role:
@@ -3089,7 +3091,7 @@ class Question:
                     if data['role'] not in self.role:
                         self.role.append(rolename)
             else:
-                raise DAError("The role of a question must be a string or a list." + self.idebug(data))
+                raise DASourceError("The role of a question must be a string or a list." + self.idebug(data))
         else:
             self.role = []
         if 'include' in data:
@@ -3109,19 +3111,19 @@ class Question:
                                     raise DANotFoundError('Question file ' + questionPath + ' not found')
                             self.interview.read_from(new_source)
                     except DANotFoundError:
-                        raise DAError('An include section could not find the file ' + str(questionPath) + '.' + self.idebug(data))
+                        raise DASourceError('An include section could not find the file ' + str(questionPath) + '.' + self.idebug(data))
             else:
-                raise DAError("An include section must be organized as a list." + self.idebug(data))
+                raise DASourceError("An include section must be organized as a list." + self.idebug(data))
         if 'action buttons' in data:
             if isinstance(data['action buttons'], dict) and len(data['action buttons']) == 1 and 'code' in data['action buttons']:
                 self.action_buttons.append(compile(data['action buttons']['code'], '<action buttons code>', 'eval'))
                 self.find_fields_in(data['action buttons']['code'])
             else:
                 if not isinstance(data['action buttons'], list):
-                    raise DAError("An action buttons specifier must be a list." + self.idebug(data))
+                    raise DASourceError("An action buttons specifier must be a list." + self.idebug(data))
                 for item in data['action buttons']:
                     if not isinstance(item, dict):
-                        raise DAError("An action buttons item must be a dictionary." + self.idebug(data))
+                        raise DASourceError("An action buttons item must be a dictionary." + self.idebug(data))
                     action = item.get('action', None)
                     target = item.get('new window', None)
                     if target is True:
@@ -3141,23 +3143,23 @@ class Question:
                     forget_prior = item.get('forget prior', False)
                     given_arguments = item.get('arguments', {})
                     if not isinstance(action, str):
-                        raise DAError("An action buttons item must contain an action in plain text." + self.idebug(data))
+                        raise DASourceError("An action buttons item must contain an action in plain text." + self.idebug(data))
                     if not isinstance(target, (str, NoneType)):
-                        raise DAError("The new window specifier in an action buttons item must refer to True or plain text." + self.idebug(data))
+                        raise DASourceError("The new window specifier in an action buttons item must refer to True or plain text." + self.idebug(data))
                     if not isinstance(given_arguments, dict):
-                        raise DAError("The arguments specifier in an action buttons item must refer to a dictionary." + self.idebug(data))
+                        raise DASourceError("The arguments specifier in an action buttons item must refer to a dictionary." + self.idebug(data))
                     if not isinstance(label, str):
-                        raise DAError("An action buttons item must contain a label in plain text." + self.idebug(data))
+                        raise DASourceError("An action buttons item must contain a label in plain text." + self.idebug(data))
                     if not isinstance(color, str):
-                        raise DAError("The color specifier in an action buttons item must refer to plain text." + self.idebug(data))
+                        raise DASourceError("The color specifier in an action buttons item must refer to plain text." + self.idebug(data))
                     if not isinstance(icon, (str, NoneType)):
-                        raise DAError("The icon specifier in an action buttons item must refer to plain text." + self.idebug(data))
+                        raise DASourceError("The icon specifier in an action buttons item must refer to plain text." + self.idebug(data))
                     if not isinstance(placement, (str, NoneType)):
-                        raise DAError("The placement specifier in an action buttons item must refer to plain text." + self.idebug(data))
+                        raise DASourceError("The placement specifier in an action buttons item must refer to plain text." + self.idebug(data))
                     if not isinstance(css_class, (str, NoneType)):
-                        raise DAError("The css classifier specifier in an action buttons item must refer to plain text." + self.idebug(data))
+                        raise DASourceError("The css classifier specifier in an action buttons item must refer to plain text." + self.idebug(data))
                     if not isinstance(forget_prior, bool):
-                        raise DAError("The forget prior specifier in an action buttons item must refer to true or false." + self.idebug(data))
+                        raise DASourceError("The forget prior specifier in an action buttons item must refer to true or false." + self.idebug(data))
                     button = {'action': TextObject(definitions + action, question=self), 'label': TextObject(definitions + label, question=self), 'color': TextObject(definitions + color, question=self)}
                     button['show if'] = showif
                     if target is not None:
@@ -3183,7 +3185,7 @@ class Question:
                     button['arguments'] = {}
                     for key, val in given_arguments.items():
                         if isinstance(val, (list, dict)):
-                            raise DAError("The arguments specifier in an action buttons item must refer to plain items." + self.idebug(data))
+                            raise DASourceError("The arguments specifier in an action buttons item must refer to plain items." + self.idebug(data))
                         if isinstance(val, str):
                             button['arguments'][key] = TextObject(definitions + val, question=self)
                         else:
@@ -3198,10 +3200,10 @@ class Question:
                 for x in data['if']:
                     self.find_fields_in(x)
             else:
-                raise DAError("An if statement must either be text or a list." + self.idebug(data))
+                raise DASourceError("An if statement must either be text or a list." + self.idebug(data))
         if 'validation code' in data:
             if not isinstance(data['validation code'], str):
-                raise DAError("A validation code statement must be text." + self.idebug(data))
+                raise DASourceError("A validation code statement must be text." + self.idebug(data))
             self.validation_code = compile(data['validation code'], f'<validation code, {self.id_debug(data)}>', 'exec')
             self.find_fields_in(data['validation code'])
         if 'require' in data:
@@ -3218,11 +3220,11 @@ class Question:
                     if isinstance(data['orelse'], dict):
                         self.or_else_question = Question(data['orelse'], self.interview, register_target=register_target, source=self.from_source, package=self.package)
                     else:
-                        raise DAError("The orelse part of a require section must be organized as a dictionary." + self.idebug(data))
+                        raise DASourceError("The orelse part of a require section must be organized as a dictionary." + self.idebug(data))
                 else:
-                    raise DAError("A require section must have an orelse part." + self.idebug(data))
+                    raise DASourceError("A require section must have an orelse part." + self.idebug(data))
             else:
-                raise DAError("A require section must be organized as a list." + self.idebug(data))
+                raise DASourceError("A require section must be organized as a list." + self.idebug(data))
         if 'attachment' in data:
             self.attachments = self.process_attachment_list(data['attachment'])
         elif 'attachments' in data:
@@ -3260,7 +3262,7 @@ class Question:
         #     elif isinstance(data['role'], str) and data['role'] not in self.role:
         #         self.role.append(data['role'])
         #     else:
-        #         raise DAError("A role section must be text or a list." + self.idebug(data))
+        #         raise DASourceError("A role section must be text or a list." + self.idebug(data))
         if 'progress' in data:
             if data['progress'] is None:
                 self.progress = -1
@@ -3338,11 +3340,11 @@ class Question:
                 self.response_code = data['response code']
         if 'css class' in data:
             if 'question' not in data:
-                raise DAError("A css class can only accompany a question." + self.idebug(data))
+                raise DASourceError("A css class can only accompany a question." + self.idebug(data))
             self.css_class = TextObject(definitions + str(data['css class']), question=self)
         if 'table css class' in data:
             if 'question' not in data:
-                raise DAError("A table css class can only accompany a question." + self.idebug(data))
+                raise DASourceError("A table css class can only accompany a question." + self.idebug(data))
             self.table_css_class = TextObject(definitions + str(data['table css class']), question=self)
         if 'question' in data:
             self.content = TextObject(definitions + str(data['question']), question=self)
@@ -3366,7 +3368,7 @@ class Question:
                             the_list = value
                         for list_item in the_list:
                             if isinstance(list_item, (dict, list, set)):
-                                raise DAError("An audio declaration in a help block can only contain a text item or a list of text items." + self.idebug(data))
+                                raise DASourceError("An audio declaration in a help block can only contain a text item or a list of text items." + self.idebug(data))
                             if self.audiovideo is None:
                                 self.audiovideo = {}
                             if 'help' not in self.audiovideo:
@@ -3379,7 +3381,7 @@ class Question:
                             the_list = value
                         for list_item in the_list:
                             if isinstance(list_item, (dict, list, set)):
-                                raise DAError("A video declaration in a help block can only contain a text item or a list of text items." + self.idebug(data))
+                                raise DASourceError("A video declaration in a help block can only contain a text item or a list of text items." + self.idebug(data))
                             if self.audiovideo is None:
                                 self.audiovideo = {}
                             if 'help' not in self.audiovideo:
@@ -3387,7 +3389,7 @@ class Question:
                             self.audiovideo['help'].append({'text': TextObject(definitions + str(list_item.strip()), question=self), 'package': self.package, 'type': 'video'})
                     if key == 'content':
                         if isinstance(value, (dict, list, set)):
-                            raise DAError("A content declaration in a help block can only contain text." + self.idebug(data))
+                            raise DASourceError("A content declaration in a help block can only contain text." + self.idebug(data))
                         self.helptext = TextObject(definitions + str(value), question=self)
             else:
                 self.helptext = TextObject(definitions + str(data['help']), question=self)
@@ -3398,7 +3400,7 @@ class Question:
                 the_list = data['audio']
             for list_item in the_list:
                 if isinstance(list_item, (dict, list, set)):
-                    raise DAError("An audio declaration can only contain a text item or a list of text items." + self.idebug(data))
+                    raise DASourceError("An audio declaration can only contain a text item or a list of text items." + self.idebug(data))
                 if self.audiovideo is None:
                     self.audiovideo = {}
                 if 'question' not in self.audiovideo:
@@ -3411,7 +3413,7 @@ class Question:
                 the_list = data['video']
             for list_item in the_list:
                 if isinstance(list_item, (dict, list, set)):
-                    raise DAError("A video declaration can only contain a text item or a list of text items." + self.idebug(data))
+                    raise DASourceError("A video declaration can only contain a text item or a list of text items." + self.idebug(data))
                 if self.audiovideo is None:
                     self.audiovideo = {}
                 if 'question' not in self.audiovideo:
@@ -3455,7 +3457,7 @@ class Question:
             else:
                 self.other_fields_used.add(data['signature'])
         elif 'required' in data:
-            raise DAError("The required modifier can only be used on a signature block" + self.idebug(data))
+            raise DASourceError("The required modifier can only be used on a signature block" + self.idebug(data))
         if 'question metadata' in data:
             self.question_metadata = recursive_textobject_or_primitive(data['question metadata'], self)
         if 'under' in data:
@@ -3469,12 +3471,12 @@ class Question:
         if 'check in' in data:
             self.interview.uses_action = True
             if isinstance(data['check in'], (dict, list, set)):
-                raise DAError("A check in event must be text or a list." + self.idebug(data))
+                raise DASourceError("A check in event must be text or a list." + self.idebug(data))
             self.checkin = str(data['check in'])
             self.names_used.add(str(data['check in']))
         if 'yesno' in data:
             if not isinstance(data['yesno'], str):
-                raise DAError("A yesno must refer to text." + self.idebug(data))
+                raise DASourceError("A yesno must refer to text." + self.idebug(data))
             self.fields.append(Field({'saveas': data['yesno'], 'boolean': 1}))
             if self.scan_for_variables:
                 self.fields_used.add(data['yesno'])
@@ -3483,7 +3485,7 @@ class Question:
             self.question_type = 'yesno'
         if 'noyes' in data:
             if not isinstance(data['noyes'], str):
-                raise DAError("A noyes must refer to text." + self.idebug(data))
+                raise DASourceError("A noyes must refer to text." + self.idebug(data))
             self.fields.append(Field({'saveas': data['noyes'], 'boolean': -1}))
             if self.scan_for_variables:
                 self.fields_used.add(data['noyes'])
@@ -3492,7 +3494,7 @@ class Question:
             self.question_type = 'noyes'
         if 'yesnomaybe' in data:
             if not isinstance(data['yesnomaybe'], str):
-                raise DAError("A yesnomaybe must refer to text." + self.idebug(data))
+                raise DASourceError("A yesnomaybe must refer to text." + self.idebug(data))
             self.fields.append(Field({'saveas': data['yesnomaybe'], 'threestate': 1}))
             if self.scan_for_variables:
                 self.fields_used.add(data['yesnomaybe'])
@@ -3501,7 +3503,7 @@ class Question:
             self.question_type = 'yesnomaybe'
         if 'noyesmaybe' in data:
             if not isinstance(data['noyesmaybe'], str):
-                raise DAError("A noyesmaybe must refer to text." + self.idebug(data))
+                raise DASourceError("A noyesmaybe must refer to text." + self.idebug(data))
             self.fields.append(Field({'saveas': data['noyesmaybe'], 'threestate': -1}))
             if self.scan_for_variables:
                 self.fields_used.add(data['noyesmaybe'])
@@ -3515,7 +3517,7 @@ class Question:
                 for key in data['sets']:
                     self.fields_used.add(key)
             else:
-                raise DAError("A sets phrase must be text or a list." + self.idebug(data))
+                raise DASourceError("A sets phrase must be text or a list." + self.idebug(data))
         if 'event' in data:
             self.interview.uses_action = True
             if isinstance(data['event'], str):
@@ -3524,20 +3526,20 @@ class Question:
                 for key in data['event']:
                     self.fields_used.add(key)
             else:
-                raise DAError("An event phrase must be text or a list." + self.idebug(data))
+                raise DASourceError("An event phrase must be text or a list." + self.idebug(data))
         if 'choices' in data or 'buttons' in data or 'dropdown' in data or 'combobox' in data:
             if 'field' in data:
                 uses_field = True
                 uses_continue_button_field = False
                 data['field'] = data['field'].strip()
                 if invalid_variable_name(data['field']):
-                    raise DAError("Missing or invalid variable name " + repr(data['field']) + "." + self.idebug(data))
+                    raise DASourceError("Missing or invalid variable name " + repr(data['field']) + "." + self.idebug(data))
             else:
                 uses_field = False
                 if 'continue button field' in data:
                     data['continue button field'] = data['continue button field'].strip()
                     if invalid_variable_name(data['continue button field']):
-                        raise DAError("Missing or invalid variable name " + repr(data['continue button field']) + "." + self.idebug(data))
+                        raise DASourceError("Missing or invalid variable name " + repr(data['continue button field']) + "." + self.idebug(data))
                     uses_continue_button_field = True
                 else:
                     uses_continue_button_field = False
@@ -3565,11 +3567,11 @@ class Question:
                 self.question_variety = 'buttons'
             if 'validation messages' in data:
                 if not isinstance(data['validation messages'], dict):
-                    raise DAError("A validation messages indicator must be a dictionary." + self.idebug(data))
+                    raise DASourceError("A validation messages indicator must be a dictionary." + self.idebug(data))
                 field_data['validation messages'] = {}
                 for validation_key, validation_message in data['validation messages'].items():
                     if not (isinstance(validation_key, str) and isinstance(validation_message, str)):
-                        raise DAError("A validation messages indicator must be a dictionary of text keys and text values." + self.idebug(data))
+                        raise DASourceError("A validation messages indicator must be a dictionary of text keys and text values." + self.idebug(data))
                     field_data['validation messages'][validation_key] = TextObject(definitions + str(validation_message).strip(), question=self)
             if uses_field:
                 if self.scan_for_variables:
@@ -3597,7 +3599,7 @@ class Question:
             self.question_type = 'multiple_choice'
         elif 'continue button field' in data and 'fields' not in data and 'yesno' not in data and 'noyes' not in data and 'yesnomaybe' not in data and 'noyesmaybe' not in data and 'signature' not in data:
             if not isinstance(data['continue button field'], str):
-                raise DAError("A continue button field must be plain text." + self.idebug(data))
+                raise DASourceError("A continue button field must be plain text." + self.idebug(data))
             if self.scan_for_variables:
                 self.fields_used.add(data['continue button field'])
             else:
@@ -3614,32 +3616,32 @@ class Question:
             elif isinstance(data['need'], list):
                 need_list = data['need']
             else:
-                raise DAError("A need phrase must be text or a list." + self.idebug(data))
+                raise DASourceError("A need phrase must be text or a list." + self.idebug(data))
             pre_need_list = []
             post_need_list = []
             for item in need_list:
                 if isinstance(item, dict):
                     if not (('pre' in item and len(item) == 1) or ('post' in item and len(item) == 1) or ('pre' in item and 'post' in item and len(item) == 2)):
-                        raise DAError("If 'need' contains a dictionary it can only include keys 'pre' or 'post'." + self.idebug(data))
+                        raise DASourceError("If 'need' contains a dictionary it can only include keys 'pre' or 'post'." + self.idebug(data))
                     if 'post' in item:
                         if isinstance(item['post'], str):
                             post_need_list.append(item['post'])
                         elif isinstance(item['post'], list):
                             post_need_list.extend(item['post'])
                         else:
-                            raise DAError("A need post phrase must be text or a list." + self.idebug(data))
+                            raise DASourceError("A need post phrase must be text or a list." + self.idebug(data))
                     if 'pre' in item:
                         if isinstance(item['pre'], str):
                             pre_need_list.append(item['pre'])
                         elif isinstance(item['pre'], list):
                             pre_need_list.extend(item['pre'])
                         else:
-                            raise DAError("A need pre phrase must be text or a list." + self.idebug(data))
+                            raise DASourceError("A need pre phrase must be text or a list." + self.idebug(data))
                 else:
                     pre_need_list.append(item)
             for sub_item in pre_need_list + post_need_list:
                 if not isinstance(sub_item, str):
-                    raise DAError("In 'need', the items must be text strings." + self.idebug(data))
+                    raise DASourceError("In 'need', the items must be text strings." + self.idebug(data))
             if len(pre_need_list) > 0:
                 try:
                     self.need = list(map((lambda x: compile(x, '<need expression>', 'eval')), pre_need_list))
@@ -3670,18 +3672,18 @@ class Question:
         if 'target' in data:
             self.interview.uses_action = True
             if isinstance(data['target'], (list, dict, set, bool, int, float)):
-                raise DAError("The target of a template must be plain text." + self.idebug(data))
+                raise DASourceError("The target of a template must be plain text." + self.idebug(data))
             if 'template' not in data:
-                raise DAError("A target directive can only be used with a template." + self.idebug(data))
+                raise DASourceError("A target directive can only be used with a template." + self.idebug(data))
             self.target = data['target']
         if 'table' in data or 'rows' in data or 'columns' in data:
             if 'table' not in data or 'rows' not in data or 'columns' not in data:
-                raise DAError("A table definition must have definitions for table, row, and column." + self.idebug(data))
+                raise DASourceError("A table definition must have definitions for table, row, and column." + self.idebug(data))
             if isinstance(data['rows'], (list, dict, set, bool, int, float)):
-                raise DAError("The row part of a table definition must be plain Python code." + self.idebug(data))
+                raise DASourceError("The row part of a table definition must be plain Python code." + self.idebug(data))
             data['rows'] = data['rows'].strip()
             if not isinstance(data['columns'], list):
-                raise DAError("The column part of a table definition must be a list." + self.idebug(data))
+                raise DASourceError("The column part of a table definition must be a list." + self.idebug(data))
             row = compile(data['rows'], '<row code>', 'eval')
             self.find_fields_in(data['rows'])
             header = []
@@ -3697,9 +3699,9 @@ class Question:
                 not_available_label = 'n/a'
             for col in data['columns']:
                 if not isinstance(col, dict):
-                    raise DAError("The column items in a table definition must be dictionaries." + self.idebug(data))
+                    raise DASourceError("The column items in a table definition must be dictionaries." + self.idebug(data))
                 if len(col) == 0:
-                    raise DAError("A column item in a table definition cannot be empty." + self.idebug(data))
+                    raise DASourceError("A column item in a table definition cannot be empty." + self.idebug(data))
                 if 'header' in col and 'cell' in col:
                     header_text = col['header']
                     cell_text = str(col['cell']).strip()
@@ -3719,12 +3721,12 @@ class Question:
                 is_editable = True
                 if isinstance(data['edit'], list):
                     if len(data['edit']) == 0:
-                        raise DAError("The edit directive must be a list of attributes, or True or False" + self.idebug(data))
+                        raise DASourceError("The edit directive must be a list of attributes, or True or False" + self.idebug(data))
                     for attribute_name in data['edit']:
                         if not isinstance(attribute_name, str):
-                            raise DAError("The edit directive must be a list of attribute names" + self.idebug(data))
+                            raise DASourceError("The edit directive must be a list of attribute names" + self.idebug(data))
                 elif not isinstance(data['edit'], bool):
-                    raise DAError("The edit directive must be a list of attributes, or True or False" + self.idebug(data))
+                    raise DASourceError("The edit directive must be a list of attributes, or True or False" + self.idebug(data))
                 keyword_args = ''
                 if 'delete buttons' in data and not data['delete buttons']:
                     keyword_args += ', delete=False'
@@ -3732,7 +3734,7 @@ class Question:
                     keyword_args += ', confirm=True'
                 if 'read only' in data:
                     if not isinstance(data['read only'], str):
-                        raise DAError("The read only directive must be plain text referring to an attribute" + self.idebug(data))
+                        raise DASourceError("The read only directive must be plain text referring to an attribute" + self.idebug(data))
                     keyword_args += ', read_only_attribute=' + repr(data['read only'].strip())
                 if isinstance(data['edit'], list):
                     column.append(compile('(' + data['rows'] + ').item_actions(row_item, row_index, ' + ', '.join([repr(y) for y in data['edit']]) + keyword_args + ', reorder=' + repr(reorder) + ', ensure_complete=' + repr(ensure_complete) + ')', '<edit code>', 'eval'))
@@ -3740,7 +3742,7 @@ class Question:
                     column.append(compile('(' + data['rows'] + ').item_actions(row_item, row_index' + keyword_args + ', reorder=' + repr(reorder) + ', ensure_complete=' + repr(ensure_complete) + ')', '<edit code>', 'eval'))
                 if 'edit header' in data:
                     if not isinstance(data['edit header'], str):
-                        raise DAError("The edit header directive must be text" + self.idebug(data))
+                        raise DASourceError("The edit header directive must be text" + self.idebug(data))
                     if data['edit header'] == '':
                         header.append(TextObject('&nbsp;'))
                     else:
@@ -3752,7 +3754,7 @@ class Question:
                 keyword_args = ''
                 if 'read only' in data:
                     if not isinstance(data['read only'], str):
-                        raise DAError("The read only directive must be plain text referring to an attribute" + self.idebug(data))
+                        raise DASourceError("The read only directive must be plain text referring to an attribute" + self.idebug(data))
                     keyword_args += ', read_only_attribute=' + repr(data['read only'].strip())
                 if 'confirm' in data and data['confirm']:
                     keyword_args += ', confirm=True'
@@ -3762,7 +3764,7 @@ class Question:
                     column.append(compile('(' + data['rows'] + ').item_actions(row_item, row_index, edit=False' + keyword_args + ', delete=False, reorder=' + repr(reorder) + ', ensure_complete=' + repr(ensure_complete) + ')', '<reorder buttons code>', 'eval'))
                 if 'edit header' in data:
                     if not isinstance(data['edit header'], str):
-                        raise DAError("The edit header directive must be text" + self.idebug(data))
+                        raise DASourceError("The edit header directive must be text" + self.idebug(data))
                     if data['edit header'] == '':
                         header.append(TextObject('&nbsp;'))
                     else:
@@ -3795,27 +3797,27 @@ class Question:
                     self.find_fields_in(data['content file']['code'])
                     self.question_type = 'template_code'
                 else:
-                    raise DAError('A content file must be specified as text, as a list of text filenames, or as a dictionary with code as the key' + self.idebug(data))
+                    raise DASourceError('A content file must be specified as text, as a list of text filenames, or as a dictionary with code as the key' + self.idebug(data))
             else:
                 if not isinstance(data['content file'], list):
                     data['content file'] = [data['content file']]
                 data['content'] = ''
                 for content_file in data['content file']:
                     if not isinstance(content_file, str):
-                        raise DAError('A content file must be specified as text, as a list of text filenames, or as a dictionary with code as the key' + self.idebug(data))
+                        raise DASourceError('A content file must be specified as text, as a list of text filenames, or as a dictionary with code as the key' + self.idebug(data))
                     file_to_read = docassemble.base.functions.package_template_filename(content_file, package=self.package)
                     # if file_to_read is not None and get_mimetype(file_to_read) != 'text/markdown':
-                    #    raise DAError('The content file ' + str(data['content file']) + ' is not a markdown file ' + str(file_to_read) + self.idebug(data))
+                    #    raise DASourceError('The content file ' + str(data['content file']) + ' is not a markdown file ' + str(file_to_read) + self.idebug(data))
                     if file_to_read is not None and os.path.isfile(file_to_read) and os.access(file_to_read, os.R_OK):
                         with open(file_to_read, 'r', encoding='utf-8') as the_file:
                             data['content'] += the_file.read()
                     else:
-                        raise DAError('Unable to read content file ' + str(data['content file']) + ' after trying to find it at ' + str(file_to_read) + self.idebug(data))
+                        raise DASourceError('Unable to read content file ' + str(data['content file']) + ' after trying to find it at ' + str(file_to_read) + self.idebug(data))
         if 'template' in data and 'content' in data:
             if isinstance(data['template'], (list, dict)):
-                raise DAError("A template must designate a single variable expressed as text." + self.idebug(data))
+                raise DASourceError("A template must designate a single variable expressed as text." + self.idebug(data))
             if isinstance(data['content'], (list, dict)):
-                raise DAError("The content of a template must be expressed as text." + self.idebug(data))
+                raise DASourceError("The content of a template must be expressed as text." + self.idebug(data))
             if self.scan_for_variables:
                 self.fields_used.add(data['template'])
             else:
@@ -3848,10 +3850,10 @@ class Question:
                     raise
                 self.find_fields_in(data['code'])
             else:
-                raise DAError("A code section must be text, not a list or a dictionary." + self.idebug(data))
+                raise DASourceError("A code section must be text, not a list or a dictionary." + self.idebug(data))
         if 'reconsider' in data:
             # if not isinstance(data['reconsider'], bool):
-            #    raise DAError("A reconsider directive must be true or false." + self.idebug(data))
+            #    raise DASourceError("A reconsider directive must be true or false." + self.idebug(data))
             if isinstance(data['reconsider'], bool):
                 if data['reconsider']:
                     if self.is_generic:
@@ -3866,10 +3868,10 @@ class Question:
                 elif isinstance(data['reconsider'], list):
                     fields = data['reconsider']
                 else:
-                    raise DAError("A reconsider directive must be true, false, a single variable or a list." + self.idebug(data))
+                    raise DASourceError("A reconsider directive must be true, false, a single variable or a list." + self.idebug(data))
                 for the_field in fields:
                     if not isinstance(the_field, str):
-                        raise DAError("A reconsider directive must refer to variable names expressed as text." + self.idebug(data))
+                        raise DASourceError("A reconsider directive must refer to variable names expressed as text." + self.idebug(data))
                     self.find_fields_in(the_field)
                     self.reconsider.append(the_field)
         if 'undefine' in data:
@@ -3878,15 +3880,15 @@ class Question:
             elif isinstance(data['undefine'], list):
                 fields = data['undefine']
             else:
-                raise DAError("A undefine directive must a single variable or a list." + self.idebug(data))
+                raise DASourceError("A undefine directive must a single variable or a list." + self.idebug(data))
             for the_field in fields:
                 if not isinstance(the_field, str):
-                    raise DAError("A undefine directive must refer to variable names expressed as text." + self.idebug(data))
+                    raise DASourceError("A undefine directive must refer to variable names expressed as text." + self.idebug(data))
                 self.find_fields_in(the_field)
                 self.undefine.append(the_field)
         if 'continue button field' in data and 'question' in data and ('field' in data or 'fields' in data or 'yesno' in data or 'noyes' in data or 'yesnomaybe' in data or 'noyesmaybe' in data or 'signature' in data):
             if not isinstance(data['continue button field'], str):
-                raise DAError("A continue button field must be plain text." + self.idebug(data))
+                raise DASourceError("A continue button field must be plain text." + self.idebug(data))
             if self.scan_for_variables:
                 self.fields_used.add(data['continue button field'])
             else:
@@ -3897,12 +3899,12 @@ class Question:
             if isinstance(data['fields'], dict):
                 data['fields'] = [data['fields']]
             if not isinstance(data['fields'], list):
-                raise DAError("The fields must be written in the form of a list." + self.idebug(data))
+                raise DASourceError("The fields must be written in the form of a list." + self.idebug(data))
             field_number = 0
             for field in data['fields']:
                 docassemble.base.functions.this_thread.misc['current_field'] = field_number
                 if not isinstance(field, dict):
-                    raise DAError("Each individual field in a list of fields must be expressed as a dictionary item, e.g., ' - Fruit: user.favorite_fruit'." + self.idebug(data))
+                    raise DASourceError("Each individual field in a list of fields must be expressed as a dictionary item, e.g., ' - Fruit: user.favorite_fruit'." + self.idebug(data))
                 manual_keys = set()
                 field_info = {'type': 'text', 'number': field_number}
                 custom_data_type = False
@@ -3910,7 +3912,7 @@ class Question:
                     if 'field' in field and 'label' not in field:
                         field['label'] = 'hidden'
                     if field.get('datatype', None) in ['file', 'files', 'camera', 'user', 'environment', 'camcorder', 'microphone']:
-                        raise DAError("Invalid datatype of hidden field." + self.idebug(data))
+                        raise DASourceError("Invalid datatype of hidden field." + self.idebug(data))
                 if 'choices' in field and isinstance(field['choices'], dict) and len(field['choices']) == 1 and 'code' in field['choices']:
                     field['code'] = field['choices']['code']
                     del field['choices']
@@ -3928,18 +3930,18 @@ class Question:
                         field['input type'] = 'hidden'
                         field['datatype'] = 'text'
                     if field['datatype'] in ('object', 'object_radio', 'multiselect', 'object_multiselect', 'checkboxes', 'object_checkboxes') and not ('choices' in field or 'code' in field):
-                        raise DAError("A multiple choice field must refer to a list of choices." + self.idebug(data))
+                        raise DASourceError("A multiple choice field must refer to a list of choices." + self.idebug(data))
                     if field['datatype'] in docassemble.base.functions.custom_types and field['datatype'] not in standard_types:
                         custom_data_type = True
                         self.interview.custom_data_types.add(field['datatype'])
                 if 'input type' in field:
                     if field['input type'] == 'ajax':
                         if 'action' not in field:
-                            raise DAError("An ajax field must have an associated action." + self.idebug(data))
+                            raise DASourceError("An ajax field must have an associated action." + self.idebug(data))
                         if 'choices' in field or 'code' in field:
-                            raise DAError("An ajax field cannot contain a list of choices except through an action." + self.idebug(data))
+                            raise DASourceError("An ajax field cannot contain a list of choices except through an action." + self.idebug(data))
                     if field['input type'] in ('radio', 'combobox', 'pulldown') and not ('choices' in field or 'code' in field):
-                        raise DAError("A multiple choice field must refer to a list of choices." + self.idebug(data))
+                        raise DASourceError("A multiple choice field must refer to a list of choices." + self.idebug(data))
                 if len(field) == 1 and 'code' in field:
                     field_info['type'] = 'fields_code'
                     self.find_fields_in(field['code'])
@@ -3950,9 +3952,9 @@ class Question:
                         del docassemble.base.functions.this_thread.misc['current_field']
                     continue
                 if 'object labeler' in field and ('datatype' not in field or not field['datatype'].startswith('object')):
-                    raise DAError("An object labeler can only be used with an object data type")
+                    raise DASourceError("An object labeler can only be used with an object data type." + self.idebug(data))
                 if ('note' in field and 'html' in field) or ('note' in field and 'raw html' in field) or ('html' in field and 'raw html' in field):
-                    raise DAError("You cannot combine note, html, and/or raw html in a single field." + self.idebug(data))
+                    raise DASourceError("You cannot combine note, html, and/or raw html in a single field." + self.idebug(data))
                 for key in field:
                     if key == 'default' and 'datatype' in field and field['datatype'] in ('object', 'object_radio', 'object_multiselect', 'object_checkboxes'):
                         continue
@@ -3996,11 +3998,11 @@ class Question:
                                 self.find_fields_in(field[key])
                     elif key == 'validation messages':
                         if not isinstance(field[key], dict):
-                            raise DAError("A validation messages indicator must be a dictionary." + self.idebug(data))
+                            raise DASourceError("A validation messages indicator must be a dictionary." + self.idebug(data))
                         field_info['validation messages'] = {}
                         for validation_key, validation_message in field[key].items():
                             if not (isinstance(validation_key, str) and isinstance(validation_message, str)):
-                                raise DAError("A validation messages indicator must be a dictionary of text keys and text values." + self.idebug(data))
+                                raise DASourceError("A validation messages indicator must be a dictionary of text keys and text values." + self.idebug(data))
                             field_info['validation messages'][validation_key] = TextObject(definitions + str(validation_message).strip(), question=self)
                     elif key == 'validate':
                         field_info['validate'] = {'compute': compile(field[key], '<validate code>', 'eval'), 'sourcecode': field[key]}
@@ -4027,7 +4029,7 @@ class Question:
                         if isinstance(field[key], list):
                             for item in field[key]:
                                 if not isinstance(item, str):
-                                    raise DAError("An allow privileges specifier must be a list of plain text items or code." + self.idebug(data))
+                                    raise DASourceError("An allow privileges specifier must be a list of plain text items or code." + self.idebug(data))
                             field_info['allow_privileges'] = field[key]
                         elif isinstance(field[key], str):
                             field_info['allow_privileges'] = [field[key]]
@@ -4035,12 +4037,12 @@ class Question:
                             field_info['allow_privileges'] = {'compute': compile(field[key]['code'], '<allow privileges code>', 'eval'), 'sourcecode': field[key]['code']}
                             self.find_fields_in(field[key]['code'])
                         else:
-                            raise DAError("An allow privileges specifier must be a list of plain text items or code." + self.idebug(data))
+                            raise DASourceError("An allow privileges specifier must be a list of plain text items or code." + self.idebug(data))
                     elif key == 'allow users' and 'datatype' in field and field['datatype'] in ('file', 'files', 'camera', 'user', 'environment'):
                         if isinstance(field[key], list):
                             for item in field[key]:
                                 if not isinstance(item, (str, int)):
-                                    raise DAError("An allow users specifier must be a list of integers and plain text items or code." + self.idebug(data))
+                                    raise DASourceError("An allow users specifier must be a list of integers and plain text items or code." + self.idebug(data))
                             field_info['allow_users'] = field[key]
                         elif isinstance(field[key], str):
                             field_info['allow_users'] = [field[key]]
@@ -4048,7 +4050,7 @@ class Question:
                             field_info['allow_users'] = {'compute': compile(field[key]['code'], '<allow users code>', 'eval'), 'sourcecode': field[key]['code']}
                             self.find_fields_in(field[key]['code'])
                         else:
-                            raise DAError("An allow users specifier must be a list of integers and plain text items or code." + self.idebug(data))
+                            raise DASourceError("An allow users specifier must be a list of integers and plain text items or code." + self.idebug(data))
                     elif key == 'persistent' and 'datatype' in field and field['datatype'] in ('file', 'files', 'camera', 'user', 'environment'):
                         if isinstance(field[key], bool):
                             field_info['persistent'] = field[key]
@@ -4086,7 +4088,7 @@ class Question:
                             self.find_fields_in(field[key])
                     elif key in ('js show if', 'js hide if'):
                         if not isinstance(field[key], str):
-                            raise DAError("A js show if or js hide if expression must be a string" + self.idebug(data))
+                            raise DASourceError("A js show if or js hide if expression must be a string" + self.idebug(data))
                         js_info = {}
                         if key == 'js show if':
                             js_info['sign'] = True
@@ -4100,7 +4102,7 @@ class Question:
                         field_info['extras']['show_if_js'] = js_info
                     elif key in ('js disable if', 'js enable if'):
                         if not isinstance(field[key], str):
-                            raise DAError("A js disable if or js enable if expression must be a string" + self.idebug(data))
+                            raise DASourceError("A js disable if or js enable if expression must be a string" + self.idebug(data))
                         js_info = {}
                         if key == 'js enable if':
                             js_info['sign'] = True
@@ -4119,16 +4121,16 @@ class Question:
                                     this_is_code = (isinstance(field[key], dict) and len(field[key]) == 1 and 'code' in field[key])
                                     other_is_code = (isinstance(field[other_key], dict) and len(field[other_key]) == 1 and 'code' in field[other_key])
                                     if this_is_code == other_is_code:
-                                        raise DAError(key + " cannot be combined with " + other_key)
+                                        raise DASourceError(key + " cannot be combined with " + other_key + "." + self.idebug(data))
                         if 'extras' not in field_info:
                             field_info['extras'] = {}
                         if isinstance(field[key], dict):
                             showif_valid = False
                             if 'variable' in field[key] and 'is' in field[key]:
                                 if 'js show if' in field or 'js hide if' in field:
-                                    raise DAError("You cannot mix js show if and non-js show if" + self.idebug(data))
+                                    raise DASourceError("You cannot mix js show if and non-js show if" + self.idebug(data))
                                 if 'js disable if' in field or 'js enable if' in field:
-                                    raise DAError("You cannot mix js disable if and non-js show if" + self.idebug(data))
+                                    raise DASourceError("You cannot mix js disable if and non-js show if" + self.idebug(data))
                                 field_info['extras']['show_if_var'] = safeid(field[key]['variable'].strip())
                                 if isinstance(field[key]['is'], str):
                                     field_info['extras']['show_if_val'] = TextObject(definitions + str(field[key]['is']).strip(), question=self)
@@ -4140,14 +4142,14 @@ class Question:
                                 self.find_fields_in(field[key]['code'])
                                 showif_valid = True
                             if not showif_valid:
-                                raise DAError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data))
+                                raise DASourceError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data))
                         elif isinstance(field[key], list):
-                            raise DAError("The keys of '" + key + "' cannot be a list" + self.idebug(data))
+                            raise DASourceError("The keys of '" + key + "' cannot be a list" + self.idebug(data))
                         elif isinstance(field[key], str):
                             field_info['extras']['show_if_var'] = safeid(field[key].strip())
                             field_info['extras']['show_if_val'] = TextObject('True')
                         else:
-                            raise DAError("Invalid variable name in show if/hide if")
+                            raise DASourceError("Invalid variable name in show if/hide if." + self.idebug(data))
                         exclusive = False
                         if isinstance(field[key], dict) and 'code' in field[key]:
                             if len(field[key]) == 1:
@@ -4173,16 +4175,16 @@ class Question:
                                     this_is_code = (isinstance(field[key], dict) and len(field[key]) == 1 and 'code' in field[key])
                                     other_is_code = (isinstance(field[other_key], dict) and len(field[other_key]) == 1 and 'code' in field[other_key])
                                     if this_is_code == other_is_code:
-                                        raise DAError(key + " cannot be combined with " + other_key)
+                                        raise DASourceError(key + " cannot be combined with " + other_key + "." + self.idebug(data))
                         if 'extras' not in field_info:
                             field_info['extras'] = {}
                         if isinstance(field[key], dict):
                             showif_valid = False
                             if 'variable' in field[key] and 'is' in field[key]:
                                 if 'js show if' in field or 'js hide if' in field:
-                                    raise DAError("You cannot mix js show if and non-js disable if" + self.idebug(data))
+                                    raise DASourceError("You cannot mix js show if and non-js disable if." + self.idebug(data))
                                 if 'js disable if' in field or 'js enable if' in field:
-                                    raise DAError("You cannot mix js disable if and non-js disable if" + self.idebug(data))
+                                    raise DASourceError("You cannot mix js disable if and non-js disable if." + self.idebug(data))
                                 field_info['extras']['show_if_var'] = safeid(field[key]['variable'].strip())
                                 if isinstance(field[key]['is'], str):
                                     field_info['extras']['show_if_val'] = TextObject(definitions + str(field[key]['is']).strip(), question=self)
@@ -4194,14 +4196,14 @@ class Question:
                                 self.find_fields_in(field[key]['code'])
                                 showif_valid = True
                             if not showif_valid:
-                                raise DAError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data))
+                                raise DASourceError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data))
                         elif isinstance(field[key], list):
-                            raise DAError("The keys of '" + key + "' cannot be a list" + self.idebug(data))
+                            raise DASourceError("The keys of '" + key + "' cannot be a list" + self.idebug(data))
                         elif isinstance(field[key], str):
                             field_info['extras']['show_if_var'] = safeid(field[key].strip())
                             field_info['extras']['show_if_val'] = TextObject('True')
                         else:
-                            raise DAError("Invalid variable name in " + key)
+                            raise DASourceError("Invalid variable name in " + key + '.' + self.idebug(data))
                         exclusive = False
                         if isinstance(field[key], dict) and 'code' in field[key]:
                             if len(field[key]) == 1:
@@ -4241,19 +4243,19 @@ class Question:
                                     auto_determine_type(field_info, the_value=field[key])
                     elif key == 'disable others':
                         if 'datatype' in field and field['datatype'] in ('file', 'files', 'range', 'multiselect', 'checkboxes', 'camera', 'user', 'environment', 'camcorder', 'microphone', 'object_multiselect', 'object_checkboxes'):  # 'yesno', 'yesnowide', 'noyes', 'noyeswide',
-                            raise DAError("A 'disable others' directive cannot be used with this data type." + self.idebug(data))
+                            raise DASourceError("A 'disable others' directive cannot be used with this data type." + self.idebug(data))
                         if not isinstance(field[key], (list, bool)):
-                            raise DAError("A 'disable others' directive must be True, False, or a list of variable names." + self.idebug(data))
+                            raise DASourceError("A 'disable others' directive must be True, False, or a list of variable names." + self.idebug(data))
                         field_info['disable others'] = field[key]
                         if field[key] is not False:
                             field_info['required'] = False
                     elif key == 'uncheck others' and 'datatype' in field and field['datatype'] in ('yesno', 'yesnowide', 'noyes', 'noyeswide'):
                         if not isinstance(field[key], (list, bool)):
-                            raise DAError("An 'uncheck others' directive must be True, False, or a list of variable names." + self.idebug(data))
+                            raise DASourceError("An 'uncheck others' directive must be True, False, or a list of variable names." + self.idebug(data))
                         field_info['uncheck others'] = field[key]
                     elif key == 'check others' and 'datatype' in field and field['datatype'] in ('yesno', 'yesnowide', 'noyes', 'noyeswide'):
                         if not isinstance(field[key], (list, bool)):
-                            raise DAError("A 'check others' directive must be True, False, or a list of variable names." + self.idebug(data))
+                            raise DASourceError("A 'check others' directive must be True, False, or a list of variable names." + self.idebug(data))
                         field_info['check others'] = field[key]
                     elif key == 'datatype':
                         field_info['type'] = field[key]
@@ -4262,7 +4264,7 @@ class Question:
                         if field[key] == 'range' and 'required' not in field_info:
                             field_info['required'] = False
                         if field[key] == 'range' and not ('min' in field and 'max' in field):
-                            raise DAError("If the datatype of a field is 'range', you must provide a min and a max." + self.idebug(data))
+                            raise DASourceError("If the datatype of a field is 'range', you must provide a min and a max." + self.idebug(data))
                         if field[key] in ('yesno', 'yesnowide', 'yesnoradio'):
                             field_info['boolean'] = 1
                         elif field[key] in ('noyes', 'noyeswide', 'noyesradio'):
@@ -4278,7 +4280,7 @@ class Question:
                         self.find_fields_in(field[key])
                         if 'exclude' in field:
                             if isinstance(field['exclude'], dict):
-                                raise DAError("An exclude entry cannot be a dictionary." + self.idebug(data))
+                                raise DASourceError("An exclude entry cannot be a dictionary." + self.idebug(data))
                             if not isinstance(field['exclude'], list):
                                 field_info['selections']['exclude'] = [compile(field['exclude'], '<expression>', 'eval')]
                                 self.find_fields_in(field['exclude'])
@@ -4294,7 +4296,7 @@ class Question:
                         elif isinstance(field[key], dict):
                             field_info['address_autocomplete'] = field[key]
                         elif isinstance(field[key], list):
-                            raise DAError("address autocomplete must be a Python expression, a dictionary, or a boolean value." + self.idebug(data))
+                            raise DASourceError("address autocomplete must be a Python expression, a dictionary, or a boolean value." + self.idebug(data))
                         else:
                             field_info['address_autocomplete'] = bool(field[key])
                     elif key == 'label above field':
@@ -4308,12 +4310,12 @@ class Question:
                         if isinstance(field[key], (str, int)):
                             field[key] = {'width': field[key]}
                         if not isinstance(field[key], dict) or len(field[key]) == 0:
-                            raise DAError(key + " is not in the correct format." + self.idebug(data))
+                            raise DASourceError(key + " is not in the correct format." + self.idebug(data))
                         for item in field[key].keys():
                             if item not in ('width', 'label width', 'offset', 'start', 'end', 'breakpoint'):
-                                raise DAError(key + " has an invalid key " + repr(item) + "." + self.idebug(data))
+                                raise DASourceError(key + " has an invalid key " + repr(item) + "." + self.idebug(data))
                         if 'width' not in field[key]:
-                            raise DAError(key + ' must specify a width.' + self.idebug(data))
+                            raise DASourceError(key + ' must specify a width.' + self.idebug(data))
                         for subkey in ('width', 'label width', 'offset'):
                             if subkey in field[key]:
                                 if isinstance(field[key][subkey], str):
@@ -4322,7 +4324,7 @@ class Question:
                                 elif isinstance(field[key][subkey], int):
                                     field_info[key][subkey] = field[key][subkey]
                                 else:
-                                    raise DAError(key + " " + subkey + " must be a number between 1 and 12, or a Python expression." + self.idebug(data))
+                                    raise DASourceError(key + " " + subkey + " must be a number between 1 and 12, or a Python expression." + self.idebug(data))
                         for subkey in ('start', 'end'):
                             if subkey in field[key]:
                                 if isinstance(field[key][subkey], str):
@@ -4331,7 +4333,7 @@ class Question:
                                 elif isinstance(field[key][subkey], (bool, NoneType)):
                                     field_info[key][subkey] = field[key][subkey]
                                 else:
-                                    raise DAError(key + " " + subkey + " must be True or False, or a Python expression." + self.idebug(data))
+                                    raise DASourceError(key + " " + subkey + " must be True or False, or a Python expression." + self.idebug(data))
                         if 'breakpoint' in field[key]:
                             field_info[key]['breakpoint'] = TextObject(DO_NOT_TRANSLATE + str(field[key]['breakpoint']), question=self)
                     elif key == 'item grid':
@@ -4339,19 +4341,19 @@ class Question:
                         if isinstance(field[key], (str, int)):
                             field[key] = {'width': field[key]}
                         if not isinstance(field[key], dict) or len(field[key]) == 0:
-                            raise DAError(key + " is not in the correct format." + self.idebug(data))
+                            raise DASourceError(key + " is not in the correct format." + self.idebug(data))
                         for item in field[key].keys():
                             if item not in ('width', 'breakpoint'):
-                                raise DAError(key + " has an invalid key " + repr(item) + "." + self.idebug(data))
+                                raise DASourceError(key + " has an invalid key " + repr(item) + "." + self.idebug(data))
                         if 'width' not in field[key]:
-                            raise DAError(key + ' must specify a width.' + self.idebug(data))
+                            raise DASourceError(key + ' must specify a width.' + self.idebug(data))
                         if isinstance(field[key]['width'], str):
                             field_info[key]['width'] = compile(field[key]['width'], '<' + key + ' width expression>', 'eval')
                             self.find_fields_in(field[key]['width'])
                         elif isinstance(field[key]['width'], int):
                             field_info[key]['width'] = field[key]['width']
                         else:
-                            raise DAError(key + " width must be a number between 1 and 12, or a Python expression." + self.idebug(data))
+                            raise DASourceError(key + " width must be a number between 1 and 12, or a Python expression." + self.idebug(data))
                         if 'breakpoint' in field[key]:
                             field_info[key]['breakpoint'] = TextObject(DO_NOT_TRANSLATE + str(field[key]['breakpoint']), question=self)
                     elif key == 'floating label':
@@ -4362,13 +4364,13 @@ class Question:
                             field_info['floating_label'] = bool(field[key])
                     elif key == 'action' and 'input type' in field and field['input type'] == 'ajax':
                         if not isinstance(field[key], str):
-                            raise DAError("An action must be plain text" + self.idebug(data))
+                            raise DASourceError("An action must be plain text" + self.idebug(data))
                         if 'combobox action' not in field_info:
                             field_info['combobox action'] = {'trig': 4}
                         field_info['combobox action']['action'] = field[key]
                     elif key == 'trigger at' and 'action' in field and 'input type' in field and field['input type'] == 'ajax':
                         if (not isinstance(field[key], int)) or field[key] < 2:
-                            raise DAError("A trigger at must an integer greater than one" + self.idebug(data))
+                            raise DASourceError("A trigger at must an integer greater than one" + self.idebug(data))
                         if 'combobox action' not in field_info:
                             field_info['combobox action'] = {}
                         field_info['combobox action']['trig'] = field[key]
@@ -4378,7 +4380,7 @@ class Question:
                         if 'datatype' in field and field['datatype'] in ('object', 'object_radio', 'object_multiselect', 'object_checkboxes'):
                             field_info['choicetype'] = 'compute'
                             if not isinstance(field[key], (list, str)):
-                                raise DAError("choices is not in appropriate format" + self.idebug(data))
+                                raise DASourceError("choices is not in appropriate format" + self.idebug(data))
                             field_info['selections'] = {}
                         else:
                             field_info['choicetype'] = 'manual'
@@ -4393,7 +4395,7 @@ class Question:
                                     manual_keys.add(item['key'])
                         if 'exclude' in field:
                             if isinstance(field['exclude'], dict):
-                                raise DAError("An exclude entry cannot be a dictionary." + self.idebug(data))
+                                raise DASourceError("An exclude entry cannot be a dictionary." + self.idebug(data))
                             if not isinstance(field['exclude'], list):
                                 self.find_fields_in(field['exclude'])
                                 field_info['selections']['exclude'] = [compile(field['exclude'].strip(), '<expression>', 'eval')]
@@ -4436,26 +4438,26 @@ class Question:
                             field_info['nota'] = TextObject(definitions + interpret_label(field[key]), question=self)
                     elif key == 'field':
                         if 'label' not in field:
-                            raise DAError("If you use 'field' to indicate a variable in a 'fields' section, you must also include a 'label.'" + self.idebug(data))
+                            raise DASourceError("If you use 'field' to indicate a variable in a 'fields' section, you must also include a 'label.'" + self.idebug(data))
                         if not isinstance(field[key], str):
-                            raise DAError("Fields in a 'field' section must be plain text." + self.idebug(data))
+                            raise DASourceError("Fields in a 'field' section must be plain text." + self.idebug(data))
                         field[key] = field[key].strip()
                         if invalid_variable_name(field[key]):
-                            raise DAError("Missing or invalid variable name " + repr(field[key]) + "." + self.idebug(data))
+                            raise DASourceError("Missing or invalid variable name " + repr(field[key]) + "." + self.idebug(data))
                         field_info['saveas'] = field[key]
                     elif key == 'label':
                         if 'field' not in field:
-                            raise DAError("If you use 'label' to label a field in a 'fields' section, you must also include a 'field.'" + self.idebug(data))
+                            raise DASourceError("If you use 'label' to label a field in a 'fields' section, you must also include a 'field.'" + self.idebug(data))
                         field_info['label'] = TextObject(definitions + interpret_label(field[key]), question=self)
                     else:
                         if 'label' in field_info:
-                            raise DAError("Syntax error: field label '" + str(key) + "' overwrites previous label, '" + str(field_info['label'].original_text) + "'" + self.idebug(data))
+                            raise DASourceError("Syntax error: field label '" + str(key) + "' overwrites previous label, '" + str(field_info['label'].original_text) + "'" + self.idebug(data))
                         field_info['label'] = TextObject(definitions + interpret_label(key), question=self)
                         if not isinstance(field[key], str):
-                            raise DAError("Fields in a 'field' section must be plain text." + self.idebug(data))
+                            raise DASourceError("Fields in a 'field' section must be plain text." + self.idebug(data))
                         field[key] = field[key].strip()
                         if invalid_variable_name(field[key]):
-                            raise DAError("Missing or invalid variable name " + repr(field[key]) + " for key " + repr(key) + "." + self.idebug(data))
+                            raise DASourceError("Missing or invalid variable name " + repr(field[key]) + " for key " + repr(key) + "." + self.idebug(data))
                         field_info['saveas'] = field[key]
                 if 'type' in field_info:
                     if field_info['type'] in ('multiselect', 'object_multiselect', 'checkboxes', 'object_checkboxes'):
@@ -4467,14 +4469,14 @@ class Question:
                         field_info['nota'] = False
                 if 'choicetype' in field_info and field_info['choicetype'] == 'compute' and 'type' in field_info and field_info['type'] in ('object', 'object_radio', 'object_multiselect', 'object_checkboxes'):
                     if 'choices' not in field:
-                        raise DAError("You need to have a choices element if you want to set a variable to an object." + self.idebug(data))
+                        raise DASourceError("You need to have a choices element if you want to set a variable to an object." + self.idebug(data))
                     if not isinstance(field['choices'], list):
                         select_list = [str(field['choices'])]
                     else:
                         select_list = field['choices']
                     if 'exclude' in field:
                         if isinstance(field['exclude'], dict):
-                            raise DAError("choices exclude list is not in appropriate format" + self.idebug(data))
+                            raise DASourceError("choices exclude list is not in appropriate format" + self.idebug(data))
                         if not isinstance(field['exclude'], list):
                             exclude_list = [str(field['exclude']).strip()]
                         else:
@@ -4483,7 +4485,7 @@ class Question:
                             select_list.append('exclude=[' + ", ".join(exclude_list) + ']')
                     if 'default' in field:
                         if not isinstance(field['default'], (list, str)):
-                            raise DAError("default list is not in appropriate format" + self.idebug(data))
+                            raise DASourceError("default list is not in appropriate format" + self.idebug(data))
                         if not isinstance(field['default'], list):
                             default_list = [str(field['default'])]
                         else:
@@ -4506,7 +4508,7 @@ class Question:
                     field_info['selections'] = {'compute': compile(source_code, '<expression>', 'eval'), 'sourcecode': source_code}
                 if 'saveas' in field_info:
                     if not isinstance(field_info['saveas'], str):
-                        raise DAError("Invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data))
+                        raise DASourceError("Invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data))
                     self.fields.append(Field(field_info))
                     if 'type' in field_info:
                         if field_info['type'] in ('multiselect', 'object_multiselect', 'checkboxes', 'object_checkboxes'):
@@ -4533,7 +4535,7 @@ class Question:
                             if re.search(r'\.text$', field_info['saveas']):
                                 field_info['saveas'] = field_info['saveas'].strip()
                                 if invalid_variable_name(field_info['saveas']):
-                                    raise DAError("Missing or invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data))
+                                    raise DASourceError("Missing or invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data))
                                 field_info['saveas'] = re.sub(r'\.text$', '', field_info['saveas'])
                                 if self.scan_for_variables:
                                     self.fields_used.add(field_info['saveas'])
@@ -4554,7 +4556,7 @@ class Question:
                             self.fields_used.add(field_info['saveas'])
                         else:
                             self.other_fields_used.add(field_info['saveas'])
-                elif 'note' in field or 'html' or 'raw html' in field:
+                elif 'note' in field or 'html' in field or 'raw html' in field:
                     if 'note' in field:
                         field_info['type'] = 'note'
                     elif 'raw html' in field:
@@ -4563,14 +4565,14 @@ class Question:
                         field_info['type'] = 'html'
                     self.fields.append(Field(field_info))
                 else:
-                    raise DAError("A field was listed without indicating a label or a variable name, and the field was not a note or raw HTML." + self.idebug(data) + " and field_info was " + repr(field_info))
+                    raise DASourceError("A field was listed without indicating a label or a variable name, and the field was not a note or raw HTML." + self.idebug(data) + " and field_info was " + repr(field_info))
                 field_number += 1
                 if 'current_field' in docassemble.base.functions.this_thread.misc:
                     del docassemble.base.functions.this_thread.misc['current_field']
         if 'review' in data:
             self.question_type = 'review'
             if self.is_mandatory and 'continue button field' not in data:
-                raise DAError("A review block without a continue button field cannot be mandatory." + self.idebug(data))
+                raise DASourceError("A review block without a continue button field cannot be mandatory." + self.idebug(data))
             if 'tabular' in data and data['tabular']:
                 if isinstance(data['tabular'], str):
                     tabular_class = data['tabular']
@@ -4580,11 +4582,11 @@ class Question:
             if isinstance(data['review'], dict):
                 data['review'] = [data['review']]
             if not isinstance(data['review'], list):
-                raise DAError("The review must be written in the form of a list." + self.idebug(data))
+                raise DASourceError("The review must be written in the form of a list." + self.idebug(data))
             field_number = 0
             for field in data['review']:
                 if not isinstance(field, dict):
-                    raise DAError("Each individual field in a list of fields must be expressed as a dictionary item, e.g., ' - Fruit: user.favorite_fruit'." + self.idebug(data))
+                    raise DASourceError("Each individual field in a list of fields must be expressed as a dictionary item, e.g., ' - Fruit: user.favorite_fruit'." + self.idebug(data))
                 field_info = {'number': field_number, 'data': []}
                 for key in field:
                     if key == 'action':
@@ -4593,7 +4595,7 @@ class Question:
                         if not isinstance(field[key], dict) and not isinstance(field[key], list):
                             field_info[key] = TextObject(definitions + str(field[key]), question=self)
                         if 'button' in field:  # or 'css' in field or 'script' in field:
-                            raise DAError("In a review block, you cannot mix help text with a button item." + self.idebug(data))  # , css, or script
+                            raise DASourceError("In a review block, you cannot mix help text with a button item." + self.idebug(data))  # , css, or script
                     elif key == 'button':
                         if not isinstance(field[key], dict) and not isinstance(field[key], list):
                             field_info['help'] = TextObject(definitions + str(field[key]), question=self)
@@ -4612,10 +4614,10 @@ class Question:
                         field_data = []
                         for the_saveas in field_list:
                             # if not isinstance(the_saveas, str):
-                            #    raise DAError("Invalid variable name in fields." + self.idebug(data))
+                            #    raise DASourceError("Invalid variable name in fields." + self.idebug(data))
                             the_saveas = str(the_saveas).strip()
                             # if invalid_variable_name(the_saveas):
-                            #    raise DAError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data))
+                            #    raise DASourceError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data))
                             if the_saveas not in field_data:
                                 field_data.append(the_saveas)
                             self.find_fields_in(the_saveas)
@@ -4625,7 +4627,7 @@ class Question:
                             field_info['saveas_code'].extend([(compile(y, '<expression>', 'eval'), True) for y in field_list])
                     elif key in ('field', 'fields'):
                         if 'label' not in field:
-                            raise DAError("If you use 'field' or 'fields' to indicate variables in a 'review' section, you must also include a 'label.'" + self.idebug(data))
+                            raise DASourceError("If you use 'field' or 'fields' to indicate variables in a 'review' section, you must also include a 'label.'" + self.idebug(data))
                         if not isinstance(field[key], list):
                             field_list = [field[key]]
                         else:
@@ -4635,29 +4637,29 @@ class Question:
                             if isinstance(the_saveas, dict) and len(the_saveas) == 1 and ('undefine' in the_saveas or 'invalidate' in the_saveas or 'recompute' in the_saveas or 'set' in the_saveas or 'follow up' in the_saveas):
                                 if 'set' in the_saveas:
                                     if not isinstance(the_saveas['set'], list):
-                                        raise DAError("The set statement must refer to a list." + self.idebug(data))
+                                        raise DASourceError("The set statement must refer to a list." + self.idebug(data))
                                     clean_list = []
                                     for the_dict in the_saveas['set']:
                                         if not isinstance(the_dict, dict):
-                                            raise DAError("A set command must refer to a list of dicts." + self.idebug(data))
+                                            raise DASourceError("A set command must refer to a list of dicts." + self.idebug(data))
                                         for the_var, the_val in the_dict.items():
                                             if not isinstance(the_var, str):
-                                                raise DAError("A set command must refer to a list of dicts with keys as variable names." + self.idebug(data))
+                                                raise DASourceError("A set command must refer to a list of dicts with keys as variable names." + self.idebug(data))
                                             the_var_stripped = the_var.strip()
                                             if invalid_variable_name(the_var_stripped):
-                                                raise DAError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data))
+                                                raise DASourceError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data))
                                             self.find_fields_in(the_var_stripped)
                                             clean_list.append([the_var_stripped, the_val])
                                     field_info['data'].append({'action': '_da_set', 'arguments': {'variables': clean_list}})
                                 if 'follow up' in the_saveas:
                                     if not isinstance(the_saveas['follow up'], list):
-                                        raise DAError("The follow up statement must refer to a list." + self.idebug(data))
+                                        raise DASourceError("The follow up statement must refer to a list." + self.idebug(data))
                                     for var in the_saveas['follow up']:
                                         if not isinstance(var, str):
-                                            raise DAError("Invalid variable name in follow up " + repr(var) + "." + self.idebug(data))
+                                            raise DASourceError("Invalid variable name in follow up " + repr(var) + "." + self.idebug(data))
                                         var_saveas = var.strip()
                                         if invalid_variable_name(var_saveas):
-                                            raise DAError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data))
+                                            raise DASourceError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data))
                                         self.find_fields_in(var_saveas)
                                         # field_info['data'].append({'action': "_da_follow_up", 'arguments': {'action': var}})
                                         field_info['data'].append({'action': var, 'arguments': {}})
@@ -4665,14 +4667,14 @@ class Question:
                                     if command not in the_saveas:
                                         continue
                                     if not isinstance(the_saveas[command], list):
-                                        raise DAError("The " + command + " statement must refer to a list." + self.idebug(data))
+                                        raise DASourceError("The " + command + " statement must refer to a list." + self.idebug(data))
                                     clean_list = []
                                     for undef_var in the_saveas[command]:
                                         if not isinstance(undef_var, str):
-                                            raise DAError("Invalid variable name " + repr(undef_var) + " in " + command + "." + self.idebug(data))
+                                            raise DASourceError("Invalid variable name " + repr(undef_var) + " in " + command + "." + self.idebug(data))
                                         undef_saveas = undef_var.strip()
                                         if invalid_variable_name(undef_saveas):
-                                            raise DAError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data))
+                                            raise DASourceError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data))
                                         self.find_fields_in(undef_saveas)
                                         clean_list.append(undef_saveas)
                                     if command == 'invalidate':
@@ -4684,13 +4686,13 @@ class Question:
                                 continue
                             if isinstance(the_saveas, dict) and len(the_saveas) == 2 and 'action' in the_saveas and 'arguments' in the_saveas:
                                 if not isinstance(the_saveas['arguments'], dict):
-                                    raise DAError("An arguments directive must refer to a dictionary.  " + repr(data))
+                                    raise DASourceError("An arguments directive must refer to a dictionary.  " + repr(data))
                                 field_info['data'].append({'action': the_saveas['action'], 'arguments': the_saveas['arguments']})
                             if not isinstance(the_saveas, str):
-                                raise DAError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data))
+                                raise DASourceError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data))
                             the_saveas = the_saveas.strip()
                             if invalid_variable_name(the_saveas):
-                                raise DAError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data))
+                                raise DASourceError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data))
                             if the_saveas not in field_info['data']:
                                 field_info['data'].append(the_saveas)
                             self.find_fields_in(the_saveas)
@@ -4698,7 +4700,7 @@ class Question:
                             field_info['action'] = {'action': field['action'], 'arguments': {}}
                     elif key == 'label':
                         if 'field' not in field and 'fields' not in field:
-                            raise DAError("If you use 'label' to label a field in a 'review' section, you must also include a 'field' or 'fields.'" + self.idebug(data))
+                            raise DASourceError("If you use 'label' to label a field in a 'review' section, you must also include a 'field' or 'fields.'" + self.idebug(data))
                         field_info['label'] = TextObject(definitions + interpret_label(field[key]), question=self)
                     else:
                         field_info['label'] = TextObject(definitions + interpret_label(key), question=self)
@@ -4711,29 +4713,29 @@ class Question:
                             if isinstance(the_saveas, dict) and len(the_saveas) == 1 and ('undefine' in the_saveas or 'invalidate' in the_saveas or 'recompute' in the_saveas or 'set' in the_saveas or 'follow up' in the_saveas):
                                 if 'set' in the_saveas:
                                     if not isinstance(the_saveas['set'], list):
-                                        raise DAError("The set statement must refer to a list." + self.idebug(data))
+                                        raise DASourceError("The set statement must refer to a list." + self.idebug(data))
                                     clean_list = []
                                     for the_dict in the_saveas['set']:
                                         if not isinstance(the_dict, dict):
-                                            raise DAError("A set command must refer to a list of dicts." + self.idebug(data))
+                                            raise DASourceError("A set command must refer to a list of dicts." + self.idebug(data))
                                         for the_var, the_val in the_dict.items():
                                             if not isinstance(the_var, str):
-                                                raise DAError("A set command must refer to a list of dicts with keys as variable names." + self.idebug(data))
+                                                raise DASourceError("A set command must refer to a list of dicts with keys as variable names." + self.idebug(data))
                                             the_var_stripped = the_var.strip()
                                             if invalid_variable_name(the_var_stripped):
-                                                raise DAError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data))
+                                                raise DASourceError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data))
                                             self.find_fields_in(the_var_stripped)
                                             clean_list.append([the_var_stripped, the_val])
                                     field_info['data'].append({'action': '_da_set', 'arguments': {'variables': clean_list}})
                                 if 'follow up' in the_saveas:
                                     if not isinstance(the_saveas['follow up'], list):
-                                        raise DAError("The follow up statement must refer to a list." + self.idebug(data))
+                                        raise DASourceError("The follow up statement must refer to a list." + self.idebug(data))
                                     for var in the_saveas['follow up']:
                                         if not isinstance(var, str):
-                                            raise DAError("Invalid variable name in follow up " + command + "." + self.idebug(data))
+                                            raise DASourceError("Invalid variable name in follow up " + command + "." + self.idebug(data))
                                         var_saveas = var.strip()
                                         if invalid_variable_name(var_saveas):
-                                            raise DAError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data))
+                                            raise DASourceError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data))
                                         self.find_fields_in(var_saveas)
                                         # field_info['data'].append({'action': "_da_follow_up", 'arguments': {'action': var}})
                                         field_info['data'].append({'action': var, 'arguments': {}})
@@ -4741,14 +4743,14 @@ class Question:
                                     if command not in the_saveas:
                                         continue
                                     if not isinstance(the_saveas[command], list):
-                                        raise DAError("The " + command + " statement must refer to a list." + self.idebug(data))
+                                        raise DASourceError("The " + command + " statement must refer to a list." + self.idebug(data))
                                     clean_list = []
                                     for undef_var in the_saveas[command]:
                                         if not isinstance(undef_var, str):
-                                            raise DAError("Invalid variable name " + repr(undef_var) + " in fields " + command + "." + self.idebug(data))
+                                            raise DASourceError("Invalid variable name " + repr(undef_var) + " in fields " + command + "." + self.idebug(data))
                                         undef_saveas = undef_var.strip()
                                         if invalid_variable_name(undef_saveas):
-                                            raise DAError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data))
+                                            raise DASourceError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data))
                                         self.find_fields_in(undef_saveas)
                                         clean_list.append(undef_saveas)
                                     if command == 'invalidate':
@@ -4760,13 +4762,13 @@ class Question:
                                 continue
                             if isinstance(the_saveas, dict) and len(the_saveas) == 2 and 'action' in the_saveas and 'arguments' in the_saveas:
                                 if not isinstance(the_saveas['arguments'], dict):
-                                    raise DAError("An arguments directive must refer to a dictionary.  " + repr(data))
+                                    raise DASourceError("An arguments directive must refer to a dictionary.  " + repr(data))
                                 field_info['data'].append({'action': the_saveas['action'], 'arguments': the_saveas['arguments']})
                             if not isinstance(the_saveas, str):
-                                raise DAError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data))
+                                raise DASourceError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data))
                             the_saveas = the_saveas.strip()
                             if invalid_variable_name(the_saveas):
-                                raise DAError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data))
+                                raise DASourceError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data))
                             # if the_saveas not in field_info['data']:
                             field_info['data'].append(the_saveas)
                             self.find_fields_in(the_saveas)
@@ -4786,7 +4788,7 @@ class Question:
                 if len(field_info['data']) > 0 or ('type' in field_info and field_info['type'] in ('note', 'html', 'raw html')):
                     self.fields.append(Field(field_info))
                 else:
-                    raise DAError("A field in a review list was listed without indicating a label or a variable name, and the field was not a note or raw HTML." + self.idebug(field_info))
+                    raise DASourceError("A field in a review list was listed without indicating a label or a variable name, and the field was not a note or raw HTML." + self.idebug(field_info))
                 field_number += 1
         if not hasattr(self, 'question_type'):
             if len(self.attachments) > 0 and len(self.fields_used) > 0 and not hasattr(self, 'content'):
@@ -4795,7 +4797,7 @@ class Question:
                 self.question_type = 'deadend'
         if should_append:
             if not hasattr(self, 'question_type'):
-                raise DAError("No question type could be determined for this section." + self.idebug(data))
+                raise DASourceError("No question type could be determined for this section." + self.idebug(data))
             if main_list:
                 self.interview.questions_list.append(self)
             self.number = self.interview.next_number()
@@ -4803,7 +4805,7 @@ class Question:
             if hasattr(self, 'id'):
                 self.name = "ID " + self.id
                 # if self.name in self.interview.questions_by_name:
-                #     raise DAError("Question ID " + str(self.id) + " results in duplicate question name")
+                #     raise DASourceError("Question ID " + str(self.id) + " results in duplicate question name")
             else:
                 self.name = "Question_" + str(self.number)
         else:
@@ -5055,7 +5057,7 @@ class Question:
                     options['skip_undefined'] = compile(target['skip undefined'], '<skip undefined expression>', 'eval')
                     self.find_fields_in(target['skip undefined'])
                 else:
-                    raise DAError('Unknown data type in attachment skip undefined.' + self.idebug(target))
+                    raise DASourceError('Unknown data type in attachment skip undefined.' + self.idebug(target))
             else:
                 options['skip_undefined'] = False
             if 'language' in target:
@@ -5075,11 +5077,11 @@ class Question:
                     self.find_fields_in(target['redact'])
             if 'checkbox export value' in target and 'pdf template file' in target:
                 if not isinstance(target['checkbox export value'], str):
-                    raise DAError("A checkbox export value must be a string." + self.idebug(target))
+                    raise DASourceError("A checkbox export value must be a string." + self.idebug(target))
                 options['checkbox_export_value'] = TextObject(target['checkbox export value'])
             if 'decimal places' in target and 'pdf template file' in target:
                 if not isinstance(target['decimal places'], (str, int)):
-                    raise DAError("A decimal places directive must be an integer or string." + self.idebug(target))
+                    raise DASourceError("A decimal places directive must be an integer or string." + self.idebug(target))
                 options['decimal_places'] = TextObject(str(target['decimal places']))
             if 'initial yaml' in target:
                 if not isinstance(target['initial yaml'], list):
@@ -5087,7 +5089,7 @@ class Question:
                 options['initial_yaml'] = []
                 for yaml_file in target['initial yaml']:
                     if not isinstance(yaml_file, str):
-                        raise DAError('An initial yaml file must be a string.' + self.idebug(target))
+                        raise DASourceError('An initial yaml file must be a string.' + self.idebug(target))
                     options['initial_yaml'].append(FileInPackage(yaml_file, 'template', self.package))
             if 'additional yaml' in target:
                 if not isinstance(target['additional yaml'], list):
@@ -5095,19 +5097,19 @@ class Question:
                 options['additional_yaml'] = []
                 for yaml_file in target['additional yaml']:
                     if not isinstance(yaml_file, str):
-                        raise DAError('An additional yaml file must be a string.' + self.idebug(target))
+                        raise DASourceError('An additional yaml file must be a string.' + self.idebug(target))
                     options['additional_yaml'].append(FileInPackage(yaml_file, 'template', self.package))
             if 'template file' in target:
                 if not isinstance(target['template file'], str):
-                    raise DAError('The template file must be a string.' + self.idebug(target))
+                    raise DASourceError('The template file must be a string.' + self.idebug(target))
                 options['template_file'] = FileInPackage(target['template file'], 'template', self.package)
             if 'rtf template file' in target:
                 if not isinstance(target['rtf template file'], str):
-                    raise DAError('The rtf template file must be a string.' + self.idebug(target))
+                    raise DASourceError('The rtf template file must be a string.' + self.idebug(target))
                 options['rtf_template_file'] = FileInPackage(target['rtf template file'], 'template', self.package)
             if 'docx reference file' in target:
                 if not isinstance(target['docx reference file'], str):
-                    raise DAError('The docx reference file must be a string.' + self.idebug(target))
+                    raise DASourceError('The docx reference file must be a string.' + self.idebug(target))
                 options['docx_reference_file'] = FileInPackage(target['docx reference file'], 'template', self.package)
             if 'usedefs' in target:
                 if isinstance(target['usedefs'], str):
@@ -5115,17 +5117,17 @@ class Question:
                 elif isinstance(target['usedefs'], list):
                     the_list = target['usedefs']
                 else:
-                    raise DAError('The usedefs included in an attachment must be specified as a list of strings or a single string.' + self.idebug(target))
+                    raise DASourceError('The usedefs included in an attachment must be specified as a list of strings or a single string.' + self.idebug(target))
                 for def_key in the_list:
                     if not isinstance(def_key, str):
-                        raise DAError('The defs in an attachment must be strings.' + self.idebug(target))
+                        raise DASourceError('The defs in an attachment must be strings.' + self.idebug(target))
                     if def_key not in self.interview.defs:
-                        raise DAError('Referred to a non-existent def "' + def_key + '."  All defs must be defined before they are used.' + self.idebug(target))
+                        raise DASourceError('Referred to a non-existent def "' + def_key + '."  All defs must be defined before they are used.' + self.idebug(target))
                     defs.extend(self.interview.defs[def_key])
             if 'variable name' in target:
                 variable_name = target['variable name']
                 if variable_name is None:
-                    raise DAError('A variable name cannot be None.' + self.idebug(target))
+                    raise DASourceError('A variable name cannot be None.' + self.idebug(target))
                 if self.scan_for_variables:
                     self.fields_used.add(target['variable name'])
                 else:
@@ -5134,13 +5136,13 @@ class Question:
                 variable_name = "_internal['docvar'][" + str(self.interview.next_attachment_number()) + "]"
             if 'metadata' in target:
                 if not isinstance(target['metadata'], dict):
-                    raise DAError('Unknown data type ' + str(type(target['metadata'])) + ' in attachment metadata.' + self.idebug(target))
+                    raise DASourceError('Unknown data type ' + str(type(target['metadata'])) + ' in attachment metadata.' + self.idebug(target))
                 for key in target['metadata']:
                     data = target['metadata'][key]
                     if isinstance(data, list):
                         for sub_data in data:
                             if not isinstance(sub_data, str):
-                                raise DAError('Unknown data type ' + str(type(sub_data)) + ' in list in attachment metadata' + self.idebug(target))
+                                raise DASourceError('Unknown data type ' + str(type(sub_data)) + ' in list in attachment metadata' + self.idebug(target))
                         newdata = list(map((lambda x: TextObject(x, question=self)), data))
                         metadata[key] = newdata
                     elif isinstance(data, str):
@@ -5148,7 +5150,7 @@ class Question:
                     elif isinstance(data, bool):
                         metadata[key] = data
                     else:
-                        raise DAError('Unknown data type ' + str(type(data)) + ' in key in attachment metadata' + self.idebug(target))
+                        raise DASourceError('Unknown data type ' + str(type(data)) + ' in key in attachment metadata' + self.idebug(target))
             if 'raw' in target and target['raw']:
                 if 'content file' in target:
                     content_file = target['content file']
@@ -5176,20 +5178,20 @@ class Question:
                         options['content file code'] = compile(target['content file']['code'], '<content file code>', 'eval')
                         self.find_fields_in(target['content file']['code'])
                     else:
-                        raise DAError('A content file must be specified as text, a list of text filenames, or a dictionary where the one key is code' + self.idebug(target))
+                        raise DASourceError('A content file must be specified as text, a list of text filenames, or a dictionary where the one key is code' + self.idebug(target))
                 else:
                     if not isinstance(target['content file'], list):
                         target['content file'] = [target['content file']]
                     target['content'] = ''
                     for content_file in target['content file']:
                         if not isinstance(content_file, str):
-                            raise DAError('A content file must be specified as text, a list of text filenames, or a dictionary where the one key is code' + self.idebug(target))
+                            raise DASourceError('A content file must be specified as text, a list of text filenames, or a dictionary where the one key is code' + self.idebug(target))
                         file_to_read = docassemble.base.functions.package_template_filename(content_file, package=self.package)
                         if file_to_read is not None and os.path.isfile(file_to_read) and os.access(file_to_read, os.R_OK):
                             with open(file_to_read, 'r', encoding='utf-8') as the_file:
                                 target['content'] += the_file.read()
                         else:
-                            raise DAError('Unable to read content file ' + str(content_file) + ' after trying to find it at ' + str(file_to_read) + self.idebug(target))
+                            raise DASourceError('Unable to read content file ' + str(content_file) + ' after trying to find it at ' + str(file_to_read) + self.idebug(target))
             if 'pdf template file' in target and ('code' in target or 'field variables' in target or 'field code' in target or 'raw field variables' in target) and 'fields' not in target:
                 target['fields'] = {}
                 field_mode = 'manual'
@@ -5201,7 +5203,7 @@ class Question:
                         options['update_references'] = compile(target['update references'], '<expression>', 'eval')
                         self.find_fields_in(target['update references'])
                     else:
-                        raise DAError('Unknown data type in attachment "update references".' + self.idebug(target))
+                        raise DASourceError('Unknown data type in attachment "update references".' + self.idebug(target))
                 if 'fields' in target:
                     field_mode = 'manual'
                 else:
@@ -5216,9 +5218,9 @@ class Question:
                 target['fields'] = {}
             if 'fields' in target:
                 if 'pdf template file' not in target and 'docx template file' not in target:
-                    raise DAError('Fields supplied to attachment but no pdf template file or docx template file supplied' + self.idebug(target))
+                    raise DASourceError('Fields supplied to attachment but no pdf template file or docx template file supplied' + self.idebug(target))
                 if 'pdf template file' in target and 'docx template file' in target:
-                    raise DAError('You cannot use a pdf template file and a docx template file at the same time' + self.idebug(target))
+                    raise DASourceError('You cannot use a pdf template file and a docx template file at the same time' + self.idebug(target))
                 if 'pdf template file' in target:
                     template_type = 'pdf'
                     target['valid formats'] = ['pdf']
@@ -5230,37 +5232,37 @@ class Question:
                         if isinstance(target['valid formats'], str):
                             target['valid formats'] = [target['valid formats']]
                         elif not isinstance(target['valid formats'], list):
-                            raise DAError('Unknown data type in attachment valid formats.' + self.idebug(target))
+                            raise DASourceError('Unknown data type in attachment valid formats.' + self.idebug(target))
                         if 'rtf to docx' in target['valid formats']:
-                            raise DAError('Valid formats cannot include "rtf to docx" when "docx template file" is used' + self.idebug(target))
+                            raise DASourceError('Valid formats cannot include "rtf to docx" when "docx template file" is used' + self.idebug(target))
                     else:
                         target['valid formats'] = ['docx', 'pdf']
                 else:
                     template_type = ''
                 if template_type == 'docx':
                     if not isinstance(target['docx template file'], (str, dict, list)):
-                        raise DAError(template_type + ' template file supplied to attachment must be a string, dict, or list' + self.idebug(target))
+                        raise DASourceError(template_type + ' template file supplied to attachment must be a string, dict, or list' + self.idebug(target))
                     if not isinstance(target['docx template file'], list):
                         target[template_type + ' template file'] = [target['docx template file']]
                 else:
                     if not isinstance(target[template_type + ' template file'], (str, dict)):
-                        raise DAError(template_type + ' template file supplied to attachment must be a string or dict' + self.idebug(target))
+                        raise DASourceError(template_type + ' template file supplied to attachment must be a string or dict' + self.idebug(target))
                 if field_mode == 'auto':
                     options['fields'] = 'auto'
                 elif not isinstance(target['fields'], (list, dict)):
-                    raise DAError('fields supplied to attachment must be a list or dictionary' + self.idebug(target))
+                    raise DASourceError('fields supplied to attachment must be a list or dictionary' + self.idebug(target))
                 target['content'] = ''
                 if template_type == 'docx':
                     options[template_type + '_template_file'] = [FileInPackage(item, 'template', package=self.package) for item in target['docx template file']]
                     for item in target['docx template file']:
                         if not isinstance(item, (str, dict)):
-                            raise DAError('docx template file supplied to attachment must be a string or dict' + self.idebug(target))
+                            raise DASourceError('docx template file supplied to attachment must be a string or dict' + self.idebug(target))
                     template_files = []
                     for template_file in options['docx_template_file']:
                         if not template_file.is_code:
                             the_docx_path = template_file.path()
                             if the_docx_path is None or not os.path.isfile(the_docx_path):
-                                raise DAError("Missing docx template file " + template_file.original_reference())
+                                raise DASourceError("Missing docx template file " + template_file.original_reference())
                             template_files.append(the_docx_path)
                     if len(template_files) > 0:
                         if len(template_files) == 1:
@@ -5322,22 +5324,22 @@ class Question:
                             self.find_fields_in(target['code'])
                     if 'field variables' in target:
                         if not isinstance(target['field variables'], list):
-                            raise DAError('The field variables must be expressed in the form of a list' + self.idebug(target))
+                            raise DASourceError('The field variables must be expressed in the form of a list' + self.idebug(target))
                         if 'code dict' not in options:
                             options['code dict'] = {}
                         for varname in target['field variables']:
                             if not valid_variable_match.match(str(varname)):
-                                raise DAError('The variable ' + str(varname) + " cannot be used in a code list" + self.idebug(target))
+                                raise DASourceError('The variable ' + str(varname) + " cannot be used in a code list" + self.idebug(target))
                             options['code dict'][varname] = compile(varname, '<expression>', 'eval')
                             self.find_fields_in(varname)
                     if 'raw field variables' in target:
                         if not isinstance(target['raw field variables'], list):
-                            raise DAError('The raw field variables must be expressed in the form of a list' + self.idebug(target))
+                            raise DASourceError('The raw field variables must be expressed in the form of a list' + self.idebug(target))
                         if 'raw code dict' not in options:
                             options['raw code dict'] = {}
                         for varname in target['raw field variables']:
                             if not valid_variable_match.match(str(varname)):
-                                raise DAError('The variable ' + str(varname) + " cannot be used in a code list" + self.idebug(target))
+                                raise DASourceError('The variable ' + str(varname) + " cannot be used in a code list" + self.idebug(target))
                             options['raw code dict'][varname] = compile(varname, '<expression>', 'eval')
                             self.find_fields_in(varname)
                     if 'field code' in target:
@@ -5347,7 +5349,7 @@ class Question:
                             target['field code'] = [target['field code']]
                         for item in target['field code']:
                             if not isinstance(item, dict):
-                                raise DAError('The field code must be expressed in the form of a dictionary' + self.idebug(target))
+                                raise DASourceError('The field code must be expressed in the form of a dictionary' + self.idebug(target))
                             for key, val in item.items():
                                 options['code dict'][key] = compile(str(val), '<expression>', 'eval')
                                 self.find_fields_in(val)
@@ -5355,9 +5357,9 @@ class Question:
                 if isinstance(target['valid formats'], str):
                     target['valid formats'] = [target['valid formats']]
                 elif not isinstance(target['valid formats'], list):
-                    raise DAError('Unknown data type in attachment valid formats.' + self.idebug(target))
+                    raise DASourceError('Unknown data type in attachment valid formats.' + self.idebug(target))
                 if 'rtf to docx' in target['valid formats'] and 'docx' in target['valid formats']:
-                    raise DAError('Valid formats cannot include both "rtf to docx" and "docx."' + self.idebug(target))
+                    raise DASourceError('Valid formats cannot include both "rtf to docx" and "docx."' + self.idebug(target))
             else:
                 target['valid formats'] = ['*']
             if 'password' in target:
@@ -5373,7 +5375,7 @@ class Question:
                     options['persistent'] = compile(target['persistent'], '<persistent expression>', 'eval')
                     self.find_fields_in(target['persistent'])
                 else:
-                    raise DAError('Unknown data type in attachment persistent.' + self.idebug(target))
+                    raise DASourceError('Unknown data type in attachment persistent.' + self.idebug(target))
             if 'private' in target:
                 if isinstance(target['private'], bool):
                     options['private'] = target['private']
@@ -5381,7 +5383,7 @@ class Question:
                     options['private'] = compile(target['private'], '<public expression>', 'eval')
                     self.find_fields_in(target['private'])
                 else:
-                    raise DAError('Unknown data type in attachment public.' + self.idebug(target))
+                    raise DASourceError('Unknown data type in attachment public.' + self.idebug(target))
             if 'allow privileges' in target:
                 if isinstance(target['allow privileges'], dict) and len(target['allow privileges']) == 1 and 'code' in target['allow privileges'] and isinstance(target['allow privileges']['code'], str):
                     options['allow privileges'] = compile(target['allow privileges']['code'], '<allow privileges expression>', 'eval')
@@ -5390,7 +5392,7 @@ class Question:
                 elif isinstance(target['allow privileges'], list):
                     for item in target['allow privileges']:
                         if not isinstance(item, str):
-                            raise DAError('Unknown data type in attachment allow privileges.' + self.idebug(target))
+                            raise DASourceError('Unknown data type in attachment allow privileges.' + self.idebug(target))
                     options['allow privileges'] = target['allow privileges']
             if 'allow users' in target:
                 if isinstance(target['allow users'], dict) and len(target['allow users']) == 1 and 'code' in target['allow users'] and isinstance(target['allow users']['code'], str):
@@ -5400,13 +5402,13 @@ class Question:
                 elif isinstance(target['allow users'], list):
                     for item in target['allow users']:
                         if not isinstance(item, (str, int)):
-                            raise DAError('Unknown data type in attachment allow users.' + self.idebug(target))
+                            raise DASourceError('Unknown data type in attachment allow users.' + self.idebug(target))
                     options['allow users'] = target['allow users']
             if 'hyperlink style' in target:
                 if isinstance(target['hyperlink style'], str):
                     options['hyperlink_style'] = TextObject(target['hyperlink style'].strip(), question=self)
                 else:
-                    raise DAError('Unknown data type in attachment hyperlink style.' + self.idebug(target))
+                    raise DASourceError('Unknown data type in attachment hyperlink style.' + self.idebug(target))
             if 'pdf/a' in target:
                 if isinstance(target['pdf/a'], bool):
                     options['pdf_a'] = target['pdf/a']
@@ -5414,7 +5416,15 @@ class Question:
                     options['pdf_a'] = compile(target['pdf/a'], '<pdfa expression>', 'eval')
                     self.find_fields_in(target['pdf/a'])
                 else:
-                    raise DAError('Unknown data type in attachment pdf/a.' + self.idebug(target))
+                    raise DASourceError('Unknown data type in attachment pdf/a.' + self.idebug(target))
+            if 'pdftk' in target:
+                if isinstance(target['pdftk'], bool):
+                    options['pdftk'] = target['pdftk']
+                elif isinstance(target['pdftk'], str):
+                    options['pdftk'] = compile(target['pdftk'], '<pdftk expression>', 'eval')
+                    self.find_fields_in(target['pdftk'])
+                else:
+                    raise DASourceError('Unknown data type in attachment pdftk.' + self.idebug(target))
             if 'rendering font' in target and target['rendering font']:
                 options['rendering_font'] = TextObject(str(target['rendering font']), question=self)
             if 'tagged pdf' in target:
@@ -5424,11 +5434,11 @@ class Question:
                     options['tagged_pdf'] = compile(target['tagged pdf'], '<tagged pdf expression>', 'eval')
                     self.find_fields_in(target['tagged pdf'])
                 else:
-                    raise DAError('Unknown data type in attachment tagged pdf.' + self.idebug(target))
+                    raise DASourceError('Unknown data type in attachment tagged pdf.' + self.idebug(target))
             if 'content' not in target:
                 if 'content file code' in options:
                     return {'name': TextObject(target['name'], question=self), 'filename': TextObject(target['filename'], question=self), 'description': TextObject(target['description'], question=self), 'content': None, 'valid_formats': target['valid formats'], 'metadata': metadata, 'variable_name': variable_name, 'orig_variable_name': variable_name, 'options': options, 'raw': target['raw']}
-                raise DAError("No content provided in attachment." + self.idebug(target))
+                raise DASourceError("No content provided in attachment." + self.idebug(target))
             # logmessage("The content is " + str(target['content']))
             return {'name': TextObject(target['name'], question=self), 'filename': TextObject(target['filename'], question=self), 'description': TextObject(target['description'], question=self), 'content': TextObject("\n".join(defs) + "\n" + target['content'], question=self), 'valid_formats': target['valid formats'], 'metadata': metadata, 'variable_name': variable_name, 'orig_variable_name': variable_name, 'options': options, 'raw': target['raw']}
         if isinstance(orig_target, str):
@@ -6111,7 +6121,10 @@ class Question:
                     extra_amount = get_config('list collect extra count', 15)
                 for list_indexno in range(length_to_use + extra_amount):
                     new_iterators = copy.copy(iterators)
-                    new_iterators[iterator_index] = str(list_indexno)
+                    try:
+                        new_iterators[iterator_index] = str(list_indexno)
+                    except IndexError:
+                        raise DAException("list collect question needs iterator " + extras['list_iterator'] + " but it was asked in a context where there is no " + extras['list_iterator'])
                     ask_result = self.ask(user_dict, old_user_dict, the_x, new_iterators, sought, orig_sought, process_list_collect=False, test_for_objects=(list_indexno < length_to_use))
                     if hasattr(self, 'list_collect_label'):
                         extras['list_message'][list_indexno] = self.list_collect_label.text(user_dict)
@@ -6358,7 +6371,7 @@ class Question:
                             if hasattr(field, 'saveas'):
                                 parse_result = parse_var_name(from_safeid(field.saveas))
                                 if not parse_result['valid']:
-                                    raise DAError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason'])
+                                    raise DASourceError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason'])
                                 if len(parse_result['objects']) > 0:
                                     assumed_objects.add(parse_result['objects'][-1])
                                 if len(parse_result['bracket_objects']) > 0:
@@ -6539,7 +6552,7 @@ class Question:
                                 # logmessage("Doing " + the_string)
                                 exec(the_string, user_dict)
                         except Exception as err:
-                            raise DAError("Failure while processing field with datatype of object: " + err.__class__.__name__ + " " + str(err))
+                            raise DASourceError("Failure while processing field with datatype of object: " + err.__class__.__name__ + " " + str(err))
                     if hasattr(field, 'label'):
                         labels[field.number] = field.label.text(user_dict)
                     if hasattr(field, 'extras'):
@@ -6691,7 +6704,7 @@ class Question:
                     #     assumed_objects.add(m.group(1))
                     parse_result = parse_var_name(from_safeid(field.saveas))
                     if not parse_result['valid']:
-                        raise DAError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason'])
+                        raise DASourceError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason'])
                     if len(parse_result['objects']) > 0:
                         assumed_objects.add(parse_result['objects'][-1])
                     if len(parse_result['bracket_objects']) > 0:
@@ -6794,12 +6807,12 @@ class Question:
                 new_list.append(new_item)
             the_list = new_list
         if not isinstance(the_list, list):
-            raise DAError("Multiple choices need to be provided in list form.  " + self.idebug(the_list))
+            raise DASourceError("Multiple choices need to be provided in list form.  " + self.idebug(the_list))
         for the_dict in the_list:
             if not isinstance(the_dict, (dict, list)):
                 the_dict = {str(the_dict): the_dict}
             elif not isinstance(the_dict, dict):
-                raise DAError("Unknown data type for the_dict in parse_fields.  " + self.idebug(the_list))
+                raise DASourceError("Unknown data type for the_dict in parse_fields.  " + self.idebug(the_list))
             result_dict = {}
             uses_value_label = 'value' in the_dict and 'label' in the_dict
             for key, value in the_dict.items():
@@ -6850,7 +6863,7 @@ class Question:
                         result_dict['label'] = TextObject(key, question=self)
                     result_dict['key'] = value
                 else:
-                    raise DAError("Unknown data type in parse_fields:" + str(type(value)) + ".  " + self.idebug(the_list))
+                    raise DASourceError("Unknown data type in parse_fields:" + str(type(value)) + ".  " + self.idebug(the_list))
             result_list.append(result_dict)
         return (has_code, result_list)
 
@@ -6927,7 +6940,7 @@ class Question:
                         if hasattr(the_file, 'number'):
                             result['file'][doc_format] = the_file.number
                 # logmessage("finalize_attachment: returning " + attachment['variable_name'] + " from cache")
-                for key in ('template', 'field_data', 'images', 'data_strings', 'convert_to_pdf_a', 'convert_to_tagged_pdf', 'password', 'owner_password', 'template_password', 'update_references', 'permissions', 'rendering_font'):
+                for key in ('template', 'field_data', 'images', 'data_strings', 'convert_to_pdf_a', 'use_pdftk', 'convert_to_tagged_pdf', 'password', 'owner_password', 'template_password', 'update_references', 'permissions', 'rendering_font'):
                     if key in result:
                         del result[key]
                 return result
@@ -6965,10 +6978,10 @@ class Question:
                             docassemble.base.functions.set_context('pdf')
                             the_template_path = attachment['options']['pdf_template_file'].path(the_user_dict=the_user_dict)
                             if the_template_path is None:
-                                raise DAError("pdf template file " + attachment['options']['pdf_template_file'].original_reference() + " not found")
-                            the_pdf_file = docassemble.base.pdftk.fill_template(the_template_path, data_strings=result['data_strings'], images=result['images'], editable=result['editable'], pdfa=result['convert_to_pdf_a'], password=result['password'], owner_password=result['owner_password'], template_password=result['template_password'], default_export_value=default_export_value, replacement_font=result['rendering_font'])
+                                raise DASourceError("pdf template file " + attachment['options']['pdf_template_file'].original_reference() + " not found")
+                            the_pdf_file = docassemble.base.pdftk.fill_template(the_template_path, data_strings=result['data_strings'], images=result['images'], editable=result['editable'], pdfa=result['convert_to_pdf_a'], use_pdftk=result['use_pdftk'], password=result['password'], owner_password=result['owner_password'], template_password=result['template_password'], default_export_value=default_export_value, replacement_font=result['rendering_font'])
                             result['file'][doc_format], result['extension'][doc_format], result['mimetype'][doc_format] = docassemble.base.functions.server.save_numbered_file(result['filename'] + '.' + extension_of_doc_format[doc_format], the_pdf_file, yaml_file_name=self.interview.source.path)  # pylint: disable=assignment-from-none,unpacking-non-sequence
-                            for key in ('images', 'data_strings', 'convert_to_pdf_a', 'convert_to_tagged_pdf', 'password', 'owner_password', 'template_password', 'update_references', 'permissions', 'rendering_font'):
+                            for key in ('images', 'data_strings', 'convert_to_pdf_a', 'use_pdftk', 'convert_to_tagged_pdf', 'password', 'owner_password', 'template_password', 'update_references', 'permissions', 'rendering_font'):
                                 if key in result:
                                     del result[key]
                             docassemble.base.functions.reset_context()
@@ -7167,14 +7180,14 @@ class Question:
                                 try:
                                     urlretrieve(url_sanitize(str(the_filename)), temp_template_file.name)
                                 except Exception as err:
-                                    raise DAError("prepare_attachment: error downloading " + str(the_filename) + ": " + str(err))
+                                    raise DASourceError("prepare_attachment: error downloading " + str(the_filename) + ": " + str(err))
                                 the_filename = temp_template_file.name
                         else:
                             the_filename = docassemble.base.functions.package_template_filename(the_filename, package=self.package)
                     else:
                         the_filename = None
                     if the_filename is None or not os.path.isfile(the_filename):
-                        raise DAError("prepare_attachment: error obtaining template file from code: " + repr(the_orig_filename))
+                        raise DASourceError("prepare_attachment: error obtaining template file from code: " + repr(the_orig_filename))
                     (the_base, actual_extension) = os.path.splitext(the_filename)  # pylint: disable=unused-variable
                     with open(the_filename, 'r', encoding='utf-8') as the_file:
                         raw_content += the_file.read()
@@ -7228,6 +7241,13 @@ class Question:
                     result['convert_to_pdf_a'] = eval(attachment['options']['pdf_a'], the_user_dict)
             else:
                 result['convert_to_pdf_a'] = self.interview.use_pdf_a
+            if 'pdftk' in attachment['options']:
+                if isinstance(attachment['options']['pdftk'], bool):
+                    result['use_pdftk'] = attachment['options']['pdftk']
+                else:
+                    result['use_pdftk'] = eval(attachment['options']['pdftk'], the_user_dict)
+            else:
+                result['use_pdftk'] = self.interview.options.get('use pdftk', False)
             if 'rendering_font' in attachment['options']:
                 result['rendering_font'] = attachment['options']['rendering_font'].text(the_user_dict).strip()
             else:
@@ -7303,7 +7323,7 @@ class Question:
                             for docx_reference in attachment['options']['docx_template_file']:
                                 for docx_path in docx_reference.paths(the_user_dict=the_user_dict):
                                     if docx_path is None or not os.path.isfile(docx_path):
-                                        raise DAError("Missing docx template file " + docx_reference.original_reference())
+                                        raise DASourceError("Missing docx template file " + docx_reference.original_reference())
                                     docx_paths.append(docx_path)
                             if len(docx_paths) == 1:
                                 docx_path = docx_paths[0]
@@ -7645,7 +7665,7 @@ class Question:
             for key, value in sorted(data.items(), key=operator.itemgetter(1)):
                 result.append({'key': TextObject(value, question=self), 'label': TextObject(key, question=self)})
         else:
-            raise DAError("Unknown data type in manual choices selection: " + re.sub(r'[<>]', '', repr(data)))
+            raise DASourceError("Unknown data type in manual choices selection: " + re.sub(r'[<>]', '', repr(data)))
         return result
 
 
@@ -8308,9 +8328,9 @@ class Interview:
                 except Exception as errMess:
                     self.success = False
                     try:
-                        error_to_raise = DAError(f'Error reading YAML file {source.path} in the block on line {line_number}\n\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n{format_yaml_errmess(errMess, source.path, line_number)}')
+                        error_to_raise = DASourceError(f'Error reading YAML file {source.path} in the block on line {line_number}\n\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n{format_yaml_errmess(errMess, source.path, line_number)}')
                     except:
-                        error_to_raise = DAError(f'Error reading YAML file {source.path} in the block on line {line_number}\n\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n' + str(errMess.__class__.__name__))
+                        error_to_raise = DASourceError(f'Error reading YAML file {source.path} in the block on line {line_number}\n\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n' + str(errMess.__class__.__name__))
                     raise error_to_raise
                 if document is not None:
                     try:
@@ -8318,13 +8338,13 @@ class Interview:
                         self.names_used.update(question.fields_used)
                     except SyntaxException as qError:
                         self.success = False
-                        raise DAError(f"Syntax Exception: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}")
+                        raise DASourceError(f"Syntax Exception: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}")
                     except CompileException as qError:
                         self.success = False
-                        raise DAError(f"Compile Exception: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}")
+                        raise DASourceError(f"Compile Exception: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}")
                     except SyntaxError as qError:
                         self.success = False
-                        raise DAError(f"Syntax Error: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}")
+                        raise DASourceError(f"Syntax Error: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}")
             line_number += lines_in_code
         for ordering in self.id_orderings:
             if ordering['type'] == 'supersedes' and hasattr(ordering['question'], 'number'):
@@ -8574,7 +8594,7 @@ class Interview:
                 number_loops += 1
                 if number_loops > self.loop_limit:
                     docassemble.base.functions.wrap_up()
-                    raise DAError("There appears to be a circularity.  Variables involved: " + ", ".join(variables_sought) + ".")
+                    raise DASourceError("There appears to be a circularity.  Variables involved: " + ", ".join(variables_sought) + ".")
                 docassemble.base.functions.reset_gathering_mode()
                 if 'action' in interview_status.current_info:
                     # logmessage("assemble: there is an action in the current_info: " + repr(interview_status.current_info['action']))
@@ -8582,13 +8602,13 @@ class Interview:
                         for the_key in ('list', 'item', 'items'):
                             if the_key in interview_status.current_info['arguments']:
                                 if illegal_variable_name(interview_status.current_info['arguments'][the_key]):
-                                    raise DAError("Invalid name " + interview_status.current_info['arguments'][the_key])
+                                    raise DASourceError("Invalid name " + interview_status.current_info['arguments'][the_key])
                                 interview_status.current_info['action_' + the_key] = eval(interview_status.current_info['arguments'][the_key], user_dict)
                     if interview_status.current_info['action'] in ('_da_dict_remove', '_da_dict_add', '_da_dict_complete'):
                         for the_key in ('dict', 'item', 'items'):
                             if the_key in interview_status.current_info['arguments']:
                                 if illegal_variable_name(interview_status.current_info['arguments'][the_key]):
-                                    raise DAError("Invalid name " + interview_status.current_info['arguments'][the_key])
+                                    raise DASourceError("Invalid name " + interview_status.current_info['arguments'][the_key])
                                 interview_status.current_info['action_' + the_key] = eval(interview_status.current_info['arguments'][the_key], user_dict)
                 # else:
                 #    logmessage("assemble: there is no action in the current_info")
@@ -8721,7 +8741,7 @@ class Interview:
                                         interview_status.populate(the_question.ask(user_dict, old_user_dict, 'None', [], None, None))
                                         interview_status.mark_tentative_as_answered(user_dict)
                                     else:
-                                        raise DAError("An embedded question can only be a code block or a regular question block.  The question type was " + getattr(the_question, 'question_type', 'unknown'))
+                                        raise DASourceError("An embedded question can only be a code block or a regular question block.  The question type was " + getattr(the_question, 'question_type', 'unknown'))
                                 else:
                                     interview_status.populate(question.ask(user_dict, old_user_dict, 'None', [], None, None))
                                 if interview_status.question.question_type == 'continue':
@@ -8776,7 +8796,7 @@ class Interview:
                                 extra = " in " + errinfo.filename
                                 if hasattr(errinfo, 'lineno'):
                                     extra += " line " + str(errinfo.lineno)
-                                raise DAError("NameError: " + str(the_exception) + extra)
+                                raise DASourceError("NameError: " + str(the_exception) + extra)
                             del cl
                             del exc
                             del tb
@@ -8945,7 +8965,7 @@ class Interview:
                     docassemble.base.functions.reset_context()
                     # logmessage(str(the_error.args))
                     docassemble.base.functions.wrap_up()
-                    raise DAError('Got error ' + str(the_error) + " " + traceback.format_exc() + "\nHistory was " + pprint.pformat(interview_status.seeking))
+                    raise DASourceError('Got error ' + str(the_error) + " " + traceback.format_exc() + "\nHistory was " + pprint.pformat(interview_status.seeking))
                 except MandatoryQuestion:
                     # logmessage("MandatoryQuestion")
                     docassemble.base.functions.reset_context()
@@ -8967,8 +8987,8 @@ class Interview:
                         pass
                     docassemble.base.functions.wrap_up()
                     if the_question is not None:
-                        raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
-                    raise DAError("no question available: " + str(qError))
+                        raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
+                    raise DASourceError("no question available: " + str(qError))
                 except CompileException as qError:
                     # logmessage("CompileException")
                     docassemble.base.functions.reset_context()
@@ -8979,8 +8999,8 @@ class Interview:
                         pass
                     docassemble.base.functions.wrap_up()
                     if the_question is not None:
-                        raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
-                    raise DAError("no question available: " + str(qError))
+                        raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
+                    raise DASourceError("no question available: " + str(qError))
                 else:
                     docassemble.base.functions.wrap_up()
                     raise DAErrorNoEndpoint('Docassemble has finished executing all code blocks marked as initial or mandatory, and finished asking all questions marked as mandatory (if any).  It is a best practice to end your interview with a question that says goodbye and offers an Exit button.')
@@ -9021,13 +9041,13 @@ class Interview:
         if self.debug:
             seeking.append({'variable': missingVariable, 'time': time.time()})
         if recursion_depth > self.recursion_limit:
-            raise DAError("There appears to be an infinite loop.  Variables in stack are " + ", ".join(sorted(variable_stack)) + ".")
+            raise DASourceError("There appears to be an infinite loop.  Variables in stack are " + ", ".join(sorted(variable_stack)) + ".")
         # logmessage("askfor: I don't have " + str(missingVariable) + " for language " + str(language))
         # logmessage("I don't have " + str(missingVariable) + " for language " + str(language))
         origMissingVariable = missingVariable
         docassemble.base.functions.set_current_variable(origMissingVariable)
         # if missingVariable in variable_stack:
-        #     raise DAError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
+        #     raise DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
         # variable_stack.add(missingVariable)
         # found_generic = False
         # realMissingVariable = missingVariable
@@ -9078,7 +9098,7 @@ class Interview:
         while True:
             num_cycles += 1
             if num_cycles > self.loop_limit:
-                raise DAError("Infinite loop detected while looking for " + missing_var)
+                raise DASourceError("Infinite loop detected while looking for " + missing_var)
             a_question_was_skipped = False
             docassemble.base.functions.reset_gathering_mode(origMissingVariable)
             # logmessage("Starting the while loop")
@@ -9370,14 +9390,14 @@ class Interview:
                                         try:
                                             urlretrieve(url_sanitize(str(the_filename)), temp_template_file.name)
                                         except Exception as err:
-                                            raise DAError("askfor: error downloading " + str(the_filename) + ": " + str(err))
+                                            raise DASourceError("askfor: error downloading " + str(the_filename) + ": " + str(err))
                                         the_filename = temp_template_file.name
                                 else:
                                     the_filename = docassemble.base.functions.package_template_filename(the_filename, package=question.package)
                             else:
                                 the_filename = None
                             if the_filename is None or not os.path.isfile(the_filename):
-                                raise DAError("askfor: error obtaining template file from code: " + repr(the_orig_filename))
+                                raise DASourceError("askfor: error obtaining template file from code: " + repr(the_orig_filename))
                             with open(the_filename, 'r', encoding='utf-8') as the_file:
                                 raw_content += the_file.read()
                         temp_vars = {}
@@ -9530,7 +9550,7 @@ class Interview:
                             continue
                         return question.ask(user_dict, old_user_dict, the_x, iterators, missing_var, origMissingVariable)
                 if a_question_was_skipped:
-                    raise DAError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
+                    raise DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
                 if 'forgive_missing_question' in docassemble.base.functions.this_thread.misc and origMissingVariable in docassemble.base.functions.this_thread.misc['forgive_missing_question']:
                     docassemble.base.functions.pop_current_variable()
                     docassemble.base.functions.pop_event_stack(origMissingVariable)
@@ -9601,7 +9621,7 @@ class Interview:
                             extra = " in " + errinfo.filename
                             if hasattr(errinfo, 'lineno'):
                                 extra += " line " + str(errinfo.lineno)
-                            raise DAError("NameError: " + str(the_exception) + extra)
+                            raise DASourceError("NameError: " + str(the_exception) + extra)
                         del cl
                         del exc
                         del tb
@@ -9611,7 +9631,7 @@ class Interview:
                     raise
                 # newMissingVariable = str(the_exception).split("'")[1]
                 # if newMissingVariable in questions_tried and newMissingVariable in variable_stack:
-                #    raise DAError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
+                #    raise DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
                 if newMissingVariable not in questions_tried:
                     questions_tried[newMissingVariable] = set()
                 else:
@@ -9784,7 +9804,7 @@ class Interview:
                     # logmessage("Got here 2")
                     return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
                 except:
-                    # raise DAError("Problem setting that variable")
+                    # raise DASourceError("Problem setting that variable")
                     continue
             except SyntaxException as qError:
                 # logmessage("SyntaxException")
@@ -9795,8 +9815,8 @@ class Interview:
                 except:
                     pass
                 if the_question is not None:
-                    raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
-                raise DAError("no question available in askfor: " + str(qError))
+                    raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
+                raise DASourceError("no question available in askfor: " + str(qError))
             except CompileException as qError:
                 # logmessage("CompileException")
                 docassemble.base.functions.reset_context()
@@ -9806,8 +9826,8 @@ class Interview:
                 except:
                     pass
                 if the_question is not None:
-                    raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
-                raise DAError("no question available in askfor: " + str(qError))
+                    raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
+                raise DASourceError("no question available in askfor: " + str(qError))
             # except SendFileError as qError:
             #     # logmessage("Trapped SendFileError2")
             #     question_data = {'extras': {}}
@@ -9973,7 +9993,7 @@ def process_selections(data, exclude=None):
                 else:
                     logmessage("process_selections: non-label passed as label in dictionary")
     else:
-        raise DAError("Unknown data type in choices selection: " + re.sub(r'[<>]', '', repr(data)))
+        raise DASourceError("Unknown data type in choices selection: " + re.sub(r'[<>]', '', repr(data)))
     return result
 
 
@@ -10079,7 +10099,7 @@ def ensure_object_exists(saveas, datatype, the_user_dict, commands=None):
         return
     parse_result = parse_var_name(saveas)
     if not parse_result['valid']:
-        raise DAError("Variable name " + saveas + " is invalid: " + parse_result['reason'])
+        raise DASourceError("Variable name " + saveas + " is invalid: " + parse_result['reason'])
     method = None
     if parse_result['final_parts'][1] != '':
         if parse_result['final_parts'][1][0] == '.':
@@ -10732,7 +10752,7 @@ def register_jinja_filter(filter_name, func):
 def get_docx_variables(the_path):
     names = set()
     if not os.path.isfile(the_path):
-        raise DAError("Missing docx template file " + os.path.basename(the_path))
+        raise DASourceError("Missing docx template file " + os.path.basename(the_path))
     try:
         docx_template = DocxTemplate(the_path)
         docx_template.render_init()
@@ -10743,7 +10763,7 @@ def get_docx_variables(the_path):
         the_xml = docx_template.patch_xml(the_xml)
         parsed_content = the_env.parse(the_xml)
     except Exception as the_err:
-        raise DAError("There was an error parsing the docx file: " + the_err.__class__.__name__ + " " + str(the_err))
+        raise DASourceError("There was an error parsing the docx file: " + the_err.__class__.__name__ + " " + str(the_err))
     for key in jinja2meta.find_undeclared_variables(parsed_content):
         if not key.startswith('__'):
             names.add(key)

programming_language: Python
filename: pdftk.py
change_type: ModificationType.MODIFY
diff: @@ -160,7 +160,7 @@ def recursively_add_fields(fields, id_to_page, outfields, prefix='', parent_ft=N
                 outfields.append((prefix, default, pageno, rect, field_type, export_value))
 
 
-def fill_template(template, data_strings=None, data_names=None, hidden=None, readonly=None, images=None, pdf_url=None, editable=True, pdfa=False, password=None, owner_password=None, template_password=None, default_export_value=None, replacement_font=None):
+def fill_template(template, data_strings=None, data_names=None, hidden=None, readonly=None, images=None, pdf_url=None, editable=True, pdfa=False, password=None, owner_password=None, template_password=None, default_export_value=None, replacement_font=None, use_pdftk=False):
     if data_strings is None:
         data_strings = []
     if data_names is None:
@@ -219,7 +219,7 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea
     for key, val in data_strings:
         data_dict[key] = val
     pdf_file = tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".pdf", delete=False)
-    if pdfa or not editable:
+    if pdfa or not editable or use_pdftk:
         fdf = Xfdf(pdf_url, data_dict)
         # fdf = fdfgen.forge_fdf(pdf_url, data_strings, data_names, hidden, readonly)
         fdf_file = tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".xfdf", delete=False)
@@ -251,7 +251,10 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea
         if len(images) > 0:
             subprocess_arguments.append('need_appearances')
         else:
-            subprocess_arguments.append('flatten')
+            if pdfa or not editable:
+                subprocess_arguments.append('flatten')
+            else:
+                subprocess_arguments.append('need_appearances')
         completed_process = None
         try:
             completed_process = subprocess.run(subprocess_arguments, timeout=600, check=False, capture_output=True)
@@ -272,7 +275,6 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea
             pdf = Pdf.open(template, password=template_password)
         else:
             pdf = Pdf.open(template)
-        pdf.Root.AcroForm.NeedAppearances = True
         for page in pdf.pages:
             if not hasattr(page, 'Annots'):
                 continue
@@ -320,6 +322,9 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea
                         the_string = pikepdf.String(value)
                         annot.V = the_string
                         annot.DV = the_string
+        pdf.Root.AcroForm.NeedAppearances = True
+        pdf.generate_appearance_streams()
+        pdf.Root.AcroForm.NeedAppearances = True
         if len(images) == 0:
             pdf.save(pdf_file.name)
             pdf.close()

programming_language: Python
filename: develop.py
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@ import re
 from flask_wtf import FlaskForm
 from docassemble.base.functions import LazyWord as word
 from wtforms import validators, ValidationError, StringField, SubmitField, TextAreaField, SelectMultipleField, SelectField, FileField, HiddenField, RadioField, BooleanField
+from docassemble.webapp.validators import html_validator
 import packaging
 
 
@@ -37,21 +38,21 @@ def validate_package_name(form, field):  # pylint: disable=unused-argument
 
 class CreatePackageForm(FlaskForm):
     name = StringField(word('Package name'), validators=[
-        validators.DataRequired(word('Package name is required')), validate_name])
+        validators.DataRequired(word('Package name is required')), validate_name, html_validator])
     submit = SubmitField(word('Get template'))
 
 
 class CreatePlaygroundPackageForm(FlaskForm):
     name = SelectField(word('Package'), validators=[
-        validators.DataRequired(word('Package name is required')), validate_name])
+        validators.DataRequired(word('Package name is required')), validate_name, html_validator])
     submit = SubmitField(word('Get package'))
 
 
 class UpdatePackageForm(FlaskForm):
-    giturl = StringField(word('GitHub URL'))
-    gitbranch = SelectField(word('GitHub Branch'))
+    giturl = StringField(word('GitHub URL'), validators=[html_validator])
+    gitbranch = SelectField(word('GitHub Branch'), validators=[html_validator])
     zipfile = FileField(word('Zip File'))
-    pippackage = StringField(word('Package on PyPI'))
+    pippackage = StringField(word('Package on PyPI'), validators=[html_validator])
     submit = SubmitField(word('Update'))
 
 
@@ -64,7 +65,7 @@ class ConfigForm(FlaskForm):
 class PlaygroundForm(FlaskForm):
     status = StringField('Status')
     original_playground_name = StringField(word('Original Name'))
-    playground_name = StringField(word('Name'), [validators.Length(min=1, max=255)])
+    playground_name = StringField(word('Name'), [validators.Length(min=1, max=255), html_validator])
     playground_content = TextAreaField(word('Playground YAML'))
     search_term = StringField(word('Search'))
     submit = SubmitField(word('Save'))
@@ -108,7 +109,7 @@ class PlaygroundFilesEditForm(FlaskForm):
     purpose = StringField('Purpose')
     section = StringField(word('Section'))
     original_file_name = StringField(word('Original Name'))
-    file_name = StringField(word('Name'), [validators.Length(min=1, max=255)])
+    file_name = StringField(word('Name'), [validators.Length(min=1, max=255), html_validator])
     search_term = StringField(word('Search'))
     file_content = TextAreaField(word('File Text'))
     active_file = StringField(word('Active File'))
@@ -118,7 +119,7 @@ class PlaygroundFilesEditForm(FlaskForm):
 
 class RenameProject(FlaskForm):
     name = StringField(word('New Name'), validators=[
-        validators.DataRequired(word('Project name is required')), validate_project_name])
+        validators.DataRequired(word('Project name is required')), validate_project_name, html_validator])
     submit = SubmitField(word('Rename'))
 
 
@@ -128,29 +129,29 @@ class DeleteProject(FlaskForm):
 
 class NewProject(FlaskForm):
     name = StringField(word('Name'), validators=[
-        validators.DataRequired(word('Project name is required')), validate_project_name])
+        validators.DataRequired(word('Project name is required')), validate_project_name, html_validator])
     submit = SubmitField(word('Save'))
 
 
 class PullPlaygroundPackage(FlaskForm):
-    github_url = StringField(word('GitHub URL'))
-    github_branch = SelectField(word('GitHub Branch'))
-    pypi = StringField(word('PyPI package'))
+    github_url = StringField(word('GitHub URL'), validators=[html_validator])
+    github_branch = SelectField(word('GitHub Branch'), validators=[html_validator])
+    pypi = StringField(word('PyPI package'), validators=[html_validator])
     pull = SubmitField(word('Pull'))
     cancel = SubmitField(word('Cancel'))
 
 
 class PlaygroundPackagesForm(FlaskForm):
-    original_file_name = StringField(word('Original Name'))
+    original_file_name = StringField(word('Original Name'), validators=[html_validator])
     file_name = StringField(word('Package Name'), validators=[validators.Length(min=1, max=50),
                                                               validators.DataRequired(word('Package Name is required')),
-                                                              validate_package_name])
-    license = StringField(word('License'), default='The MIT License (MIT)', validators=[validators.Length(min=0, max=255)])
-    author_name = StringField(word('Author Name'), validators=[validators.Length(min=0, max=255)])
-    author_email = StringField(word('Author E-mail'), validators=[validators.Length(min=0, max=255)])
-    description = StringField(word('Description'), validators=[validators.Length(min=0, max=255)], default="A docassemble extension.")
-    version = StringField(word('Version'), validators=[validators.Length(min=0, max=255), validate_package_version], default="0.0.1")
-    url = StringField(word('URL'), validators=[validators.Length(min=0, max=255)], default="")
+                                                              validate_package_name, html_validator])
+    license = StringField(word('License'), default='The MIT License (MIT)', validators=[validators.Length(min=0, max=255), html_validator])
+    author_name = StringField(word('Author Name'), validators=[validators.Length(min=0, max=255), html_validator])
+    author_email = StringField(word('Author E-mail'), validators=[validators.Length(min=0, max=255), html_validator])
+    description = StringField(word('Description'), validators=[validators.Length(min=0, max=255), html_validator], default="A docassemble extension.")
+    version = StringField(word('Version'), validators=[validators.Length(min=0, max=255), validate_package_version, html_validator], default="0.0.1")
+    url = StringField(word('URL'), validators=[validators.Length(min=0, max=255), html_validator], default="")
     dependencies = SelectMultipleField(word('Dependencies'))
     interview_files = SelectMultipleField(word('Interview files'))
     template_files = SelectMultipleField(word('Template files'))
@@ -159,7 +160,7 @@ class PlaygroundPackagesForm(FlaskForm):
     sources_files = SelectMultipleField(word('Source files'))
     readme = TextAreaField(word('README file'), default='')
     github_branch = NonValidatingSelectField(word('Branch'))
-    github_branch_new = StringField(word('Name of new branch'))
+    github_branch_new = StringField(word('Name of new branch'), validators=[html_validator])
     commit_message = StringField(word('Commit message'), default="")
     pypi_also = BooleanField(word('Publish on PyPI also'))
     install_also = BooleanField(word('Install package on this server also'))
@@ -222,7 +223,7 @@ class APIKey(FlaskForm):
     action = HiddenField()
     key = HiddenField()
     security = HiddenField()
-    name = StringField(word('Name'), validators=[validators.Length(min=1, max=255)])
+    name = StringField(word('Name'), validators=[validators.Length(min=1, max=255), html_validator])
     method = SelectField(word('Security Method'))
     permissions = SelectMultipleField(word('Limited Permissions'))
     submit = SubmitField(word('Create'))

programming_language: Python
filename: server.py
change_type: ModificationType.MODIFY
diff: @@ -49,7 +49,7 @@ import docassemble.webapp.setup
 from docassemble.webapp.setup import da_version
 import docassemble.base.astparser
 from docassemble.webapp.api_key import encrypt_api_key
-from docassemble.base.error import DAError, DAErrorNoEndpoint, DAErrorMissingVariable, DAErrorCompileError, DAValidationError, DAException, DANotFoundError, DAInvalidFilename
+from docassemble.base.error import DAError, DAErrorNoEndpoint, DAErrorMissingVariable, DAErrorCompileError, DAValidationError, DAException, DANotFoundError, DAInvalidFilename, DASourceError
 import docassemble.base.functions
 from docassemble.base.functions import get_default_timezone, ReturnValue, word
 import docassemble.base.DA
@@ -159,7 +159,6 @@ docassemble.base.util.set_random_forest_machine_learner(docassemble.webapp.machi
 docassemble.base.util.set_svm_machine_learner(docassemble.webapp.machinelearning.SVMMachineLearner)
 
 
-
 min_system_version = '1.2.0'
 re._MAXCACHE = 10000
 
@@ -1178,12 +1177,17 @@ def my_default_url(error, endpoint, values):  # pylint: disable=unused-argument
 
 
 def make_safe_url(url):
+    if url in ('help', 'login', 'signin', 'restart', 'new_session', 'exit', 'interview', 'logout', 'exit_logout', 'leave', 'register', 'profile', 'change_password', 'interviews', 'dispatch', 'manage', 'config', 'playground', 'playgroundtemplate', 'playgroundstatic', 'playgroundsources', 'playgroundmodules', 'playgroundpackages', 'configuration', 'root', 'temp_url', 'login_url', 'exit_endpoint', 'interview_start', 'interview_list', 'playgroundfiles', 'create_playground_package', 'run', 'run_interview_in_package', 'run_dispatch', 'run_new', 'run_new_dispatch'):
+        return url
     parts = urlsplit(url)
     safe_url = parts.path
     if parts.query != '':
         safe_url += '?' + parts.query
     if parts.fragment != '':
         safe_url += '#' + parts.fragment
+    if len(safe_url) > 0 and safe_url[0] not in ('?', '#', '/'):
+        safe_url = '/' + safe_url
+    safe_url = re.sub(r'^//+', '/', safe_url)
     return safe_url
 
 
@@ -1238,6 +1242,7 @@ lm.anonymous_user = AnonymousUserModel
 if DEBUG_BOOT:
     boot_log("server: finished setting up Flask")
 
+
 def url_for_interview(**args):
     for k, v in daconfig.get('dispatch').items():
         if v == args['i']:
@@ -5840,7 +5845,7 @@ def github_oauth_callback():
         return ('File not found', 404)
     setup_translation()
     failed = False
-    do_redirect = False
+    do_a_redirect = False
     if not app.config['USE_GITHUB']:
         logmessage('github_oauth_callback: server does not use github')
         failed = True
@@ -5853,14 +5858,14 @@ def github_oauth_callback():
         if 'code' not in request.args or 'state' not in request.args:
             logmessage('github_oauth_callback: code and state not in args')
             failed = True
-            do_redirect = True
+            do_a_redirect = True
         elif request.args['state'] != github_next['state']:
             logmessage('github_oauth_callback: state did not match')
             failed = True
     if failed:
         r.delete('da:github:userid:' + str(current_user.id))
         r.delete('da:using_github:userid:' + str(current_user.id))
-        if do_redirect:
+        if do_a_redirect:
             flash(word("There was a problem connecting to GitHub. Please check your GitHub configuration and try again."), 'danger')
             return redirect(url_for('github_menu'))
         return ('File not found', 404)
@@ -8265,7 +8270,7 @@ def index(action_argument=None, refer=None):
                     the_field = validation_error.field
                     logmessage("field is " + the_field)
                     if the_field not in key_to_orig_key:
-                        for item in key_to_orig_key.keys():
+                        for item in key_to_orig_key:
                             if item.startswith(the_field + '['):
                                 the_field = item
                                 break
@@ -10915,7 +10920,7 @@ def index(action_argument=None, refer=None):
         $(query).each(function(){
           var showIfParent = $(this).parents('.dashowif,.dajsshowif');
           if (!(showIfParent.length && ($(showIfParent[0]).data('isVisible') == '0' || !$(showIfParent[0]).is(":visible")))){
-            if ($(this).hasClass('combobox')){
+            if ($(this).prop('tagName') == 'INPUT' && $(this).hasClass('combobox')){
               if (value){
                 daComboBoxes[$(this).attr('id')].disable();
               }
@@ -14001,7 +14006,7 @@ def observer():
         $(query).each(function(){
           var showIfParent = $(this).parents('.dashowif, .dajsshowif');
           if (!(showIfParent.length && ($(showIfParent[0]).data('isVisible') == '0' || !$(showIfParent[0]).is(":visible")))){
-            if ($(this).hasClass('combobox')){
+            if ($(this).prop('tagName') == 'INPUT' && $(this).hasClass('combobox')){
               if (value){
                 daComboBoxes[$(this).attr('id')].disable();
               }
@@ -23043,7 +23048,14 @@ def server_error(the_error):
     else:
         the_history = None
     the_vars = None
-    if isinstance(the_error, (DAError, DANotFoundError, DAInvalidFilename)):
+    if isinstance(the_error, DASourceError):
+        if (DEBUG and daconfig.get('development site is protected', False)) or (current_user.is_authenticated and current_user.has_role('admin', 'developer')):
+            errmess = str(the_error)
+        else:
+            errmess = word("There was an error. Please contact the system administrator.")
+        the_trace = None
+        logmessage(str(the_error))
+    elif isinstance(the_error, (DAError, DANotFoundError, DAInvalidFilename)):
         errmess = str(the_error)
         the_trace = None
         logmessage(errmess)
@@ -23073,7 +23085,10 @@ def server_error(the_error):
             errmess += "\nIn field index number " + str(docassemble.base.functions.this_thread.misc['current_field'])
         if hasattr(the_error, 'da_line_with_error'):
             errmess += "\nIn line: " + str(the_error.da_line_with_error)
-
+        try:
+            logmessage(errmess)
+        except:
+            logmessage("Could not log the error message")
         logmessage(the_trace)
     if isinstance(the_error, DAError):
         error_code = the_error.error_code
@@ -23296,7 +23311,7 @@ def server_error(the_error):
         if 'in error' not in session and docassemble.base.functions.this_thread.interview is not None and 'error action' in docassemble.base.functions.this_thread.interview.consolidated_metadata:
             session['in error'] = True
             return index(action_argument={'action': docassemble.base.functions.this_thread.interview.consolidated_metadata['error action'], 'arguments': {'error_message': orig_errmess, 'error_history': the_history, 'error_trace': the_trace}}, refer=['error'])
-    show_debug = not bool((not DEBUG) and isinstance(the_error, (DAError, DAInvalidFilename)))
+    show_debug = not bool((not (DEBUG and daconfig.get('development site is protected', False))) and isinstance(the_error, (DAError, DAInvalidFilename)))
     if int(int(error_code)/100) == 4:
         show_debug = False
     if error_code == 404:
@@ -27000,6 +27015,7 @@ def invite_user(email_address, privilege=None, send=True):
         return None
     return accept_invite_link
 
+
 @app.route('/api/user_invite', methods=['POST'])
 @csrf.exempt
 @cross_origin(origins='*', methods=['POST', 'HEAD'], automatic_options=True)

programming_language: Python
filename: forms.py
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,9 @@ from flask_wtf import FlaskForm
 from wtforms import DateField, StringField, SubmitField, ValidationError, BooleanField, SelectField, SelectMultipleField, HiddenField, validators, TextAreaField
 from wtforms.validators import DataRequired, Email, Optional
 from wtforms.widgets import PasswordInput
+from flask import flash, current_app, request, abort
+from flask_login import current_user
+from sqlalchemy import select
 from docassemble.base.functions import LazyWord as word, LazyArray
 from docassemble.base.config import daconfig
 from docassemble.base.generate_key import random_alphanumeric
@@ -12,9 +15,7 @@ from docassemble.base.logger import logmessage
 from docassemble.webapp.daredis import r
 from docassemble.webapp.db_object import db
 from docassemble.webapp.users.models import UserModel, Role
-from flask import flash, current_app, request, abort
-from flask_login import current_user
-from sqlalchemy import select
+from docassemble.webapp.validators import html_validator
 try:
     import ldap
 except ImportError:
@@ -207,21 +208,22 @@ def da_registration_restrict_validator(form, field):  # pylint: disable=unused-a
 
 
 class MyRegisterForm(RegisterForm):
-    first_name = StringField(word('First name'), [validators.Length(min=0, max=255)])
-    last_name = StringField(word('Last name'), [validators.Length(min=0, max=255)])
-    country = StringField(word('Country code'), [validators.Length(min=0, max=2)])
-    subdivisionfirst = StringField(word('First subdivision'), [validators.Length(min=0, max=64)])
-    subdivisionsecond = StringField(word('Second subdivision'), [validators.Length(min=0, max=64)])
-    subdivisionthird = StringField(word('Third subdivision'), [validators.Length(min=0, max=64)])
-    organization = StringField(word('Organization'), [validators.Length(min=0, max=64)])
-    language = StringField(word('Language'), [validators.Length(min=0, max=64)])
-    timezone = SelectField(word('Time Zone'), [validators.Length(min=0, max=64)])
-    nickname = StringField(word('Nickname'), [fix_nickname])
+    first_name = StringField(word('First name'), [validators.Length(min=0, max=255), html_validator])
+    last_name = StringField(word('Last name'), [validators.Length(min=0, max=255), html_validator])
+    country = StringField(word('Country code'), [validators.Length(min=0, max=2), html_validator])
+    subdivisionfirst = StringField(word('First subdivision'), [validators.Length(min=0, max=64), html_validator])
+    subdivisionsecond = StringField(word('Second subdivision'), [validators.Length(min=0, max=64), html_validator])
+    subdivisionthird = StringField(word('Third subdivision'), [validators.Length(min=0, max=64), html_validator])
+    organization = StringField(word('Organization'), [validators.Length(min=0, max=64), html_validator])
+    language = StringField(word('Language'), [validators.Length(min=0, max=64), html_validator])
+    timezone = SelectField(word('Time Zone'), [validators.Length(min=0, max=64), html_validator])
+    nickname = StringField(word('Nickname'), [fix_nickname, html_validator])
     email = StringField(word('Email'), validators=[
         validators.DataRequired(word('Email is required')),
         validators.Email(word('Invalid Email')),
         da_unique_email_validator,
-        da_registration_restrict_validator])
+        da_registration_restrict_validator,
+        html_validator])
 
 
 def length_two(form, field):  # pylint: disable=unused-argument
@@ -236,16 +238,16 @@ class NewPrivilegeForm(FlaskForm):
 
 
 class UserProfileForm(FlaskForm):
-    first_name = StringField(word('First name'), [validators.Length(min=0, max=255)])
-    last_name = StringField(word('Last name'), [validators.Length(min=0, max=255)])
-    country = StringField(word('Country code'), [validators.Length(min=0, max=2)])
-    subdivisionfirst = StringField(word('First subdivision'), [validators.Length(min=0, max=64)])
-    subdivisionsecond = StringField(word('Second subdivision'), [validators.Length(min=0, max=64)])
-    subdivisionthird = StringField(word('Third subdivision'), [validators.Length(min=0, max=64)])
-    organization = StringField(word('Organization'), [validators.Length(min=0, max=64)])
-    language = StringField(word('Language'), [validators.Length(min=0, max=64)])
-    timezone = SelectField(word('Time Zone'), [validators.Length(min=0, max=64)])
-    pypi_username = StringField(word('PyPI Username'), [validators.Length(min=0, max=255)])
+    first_name = StringField(word('First name'), [validators.Length(min=0, max=255), html_validator])
+    last_name = StringField(word('Last name'), [validators.Length(min=0, max=255), html_validator])
+    country = StringField(word('Country code'), [validators.Length(min=0, max=2), html_validator])
+    subdivisionfirst = StringField(word('First subdivision'), [validators.Length(min=0, max=64), html_validator])
+    subdivisionsecond = StringField(word('Second subdivision'), [validators.Length(min=0, max=64), html_validator])
+    subdivisionthird = StringField(word('Third subdivision'), [validators.Length(min=0, max=64), html_validator])
+    organization = StringField(word('Organization'), [validators.Length(min=0, max=64), html_validator])
+    language = StringField(word('Language'), [validators.Length(min=0, max=64), html_validator])
+    timezone = SelectField(word('Time Zone'), [validators.Length(min=0, max=64), html_validator])
+    pypi_username = StringField(word('PyPI Username'), [validators.Length(min=0, max=255), html_validator])
     pypi_password = StringField(word('PyPI Password'), [validators.Length(min=0, max=255)])
     confirmed_at = DateField(word('Confirmation Date'))
     submit = SubmitField(word('Save'))
@@ -253,7 +255,7 @@ class UserProfileForm(FlaskForm):
 
 
 class EditUserProfileForm(UserProfileForm):
-    email = StringField(word('E-mail'))
+    email = StringField(word('E-mail'), validators=[Email(word('Must be a valid e-mail address')), html_validator])
     role_id = SelectMultipleField(word('Privileges'), coerce=int)
     active = BooleanField(word('Active'))
     uses_mfa = BooleanField(word('Uses two-factor authentication'))
@@ -299,11 +301,11 @@ class PhoneUserProfileForm(UserProfileForm):
                     flash(word("Please choose a different e-mail address."), 'error')
                     return False
         return super().validate()
-    email = StringField(word('E-mail'), validators=[Optional(), Email(word('Must be a valid e-mail address'))])
+    email = StringField(word('E-mail'), validators=[Optional(), Email(word('Must be a valid e-mail address')), html_validator])
 
 
 class RequestDeveloperForm(FlaskForm):
-    reason = StringField(word('Reason for needing developer account (optional)'))
+    reason = StringField(word('Reason for needing developer account (optional)'), validators=[html_validator])
     submit = SubmitField(word('Submit'))
 
 
@@ -334,21 +336,21 @@ class UserAddForm(FlaskForm):
     email = StringField(word('E-mail'), validators=[
         validators.InputRequired(word('E-mail is required')),
         validators.Email(word('Invalid E-mail'))])
-    first_name = StringField(word('First name'), [validators.Length(min=0, max=255)])
-    last_name = StringField(word('Last name'), [validators.Length(min=0, max=255)])
+    first_name = StringField(word('First name'), [validators.Length(min=0, max=255), html_validator])
+    last_name = StringField(word('Last name'), [validators.Length(min=0, max=255), html_validator])
     role_id = SelectMultipleField(word('Privileges'), coerce=int)
     password = StringField(word('Password'), widget=PasswordInput(hide_value=False), validators=[password_validator])
     submit = SubmitField(word('Add'))
 
 
 class PhoneLoginForm(FlaskForm):
-    phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255)])
+    phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255), html_validator])
     submit = SubmitField(word('Go'))
 
 
 class PhoneLoginVerifyForm(FlaskForm):
-    phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255)])
-    verification_code = StringField(word('Verification code'), [validators.Length(min=daconfig['verification code digits'], max=daconfig['verification code digits'])])
+    phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255), html_validator])
+    verification_code = StringField(word('Verification code'), [validators.Length(min=daconfig['verification code digits'], max=daconfig['verification code digits']), html_validator])
     submit = SubmitField(word('Verify'))
 
     def validate(self):  # pylint: disable=arguments-differ
@@ -404,7 +406,7 @@ class MFAChooseForm(FlaskForm):
 
 
 class MFASMSSetupForm(FlaskForm):
-    phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255)])
+    phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255), html_validator])
     submit = SubmitField(word('Verify'))
 
 
@@ -416,8 +418,8 @@ class MFAVerifySMSSetupForm(FlaskForm):
 class MyResendConfirmEmailForm(FlaskForm):
     email = StringField(word('Your e-mail address'), validators=[
         validators.DataRequired(word('E-mail address is required')),
-        validators.Email(word('Invalid e-mail address')),
-        ])
+        validators.Email(word('Invalid e-mail address'))
+    ])
     submit = SubmitField(word('Send confirmation email'))
 
 

programming_language: Python
filename: validators.py
change_type: ModificationType.ADD
diff: @@ -0,0 +1,10 @@
+from bs4 import BeautifulSoup
+from wtforms import ValidationError
+from docassemble.base.functions import LazyWord as word
+
+
+def html_validator(form, field):  # pylint: disable=unused-argument
+    """Field must not contain HTML"""
+    text = BeautifulSoup(field.data, "html.parser").get_text('')
+    if text != field.data:
+        raise ValidationError(word('Field cannot contain HTML'))

programming_language: Python