CVE-2024-27291: Insufficient Information (NVD-CWE-noinfo)
value: 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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-21T02:52Z
Repository: https://github.com/jhpyle/docassemble
Language: Python
Files changed: CHANGELOG.md, Dockerfile, docassemble_base/docassemble/base/data/sources/base-words.yml, docassemble_base/docassemble/base/error.py, docassemble_base/docassemble/base/pandoc.py, docassemble_base/docassemble/base/parse.py, docassemble_base/docassemble/base/pdftk.py, docassemble_webapp/docassemble/webapp/develop.py, docassemble_webapp/docassemble/webapp/server.py, docassemble_webapp/docassemble/webapp/users/forms.py, docassemble_webapp/docassemble/webapp/validators.py
Commit date: 2024-02-29 12:43:07 -0500
Commit message: 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

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d14b7b3f..1504b6c4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,34 @@
 # 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
 - Issue with installation of SSL certificates stored on S3.
 
diff --git a/Dockerfile b/Dockerfile
index 7167ed8af..c66bb1b1a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -120,11 +120,17 @@ USER www-data
 RUN bash -c \
 "source /usr/share/docassemble/local3.10/bin/activate \
 && 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
 
 EXPOSE 80 443 9001 514 25 465 8080 8081 8082 5432 6379 4369 5671 5672 25672
diff --git a/docassemble_base/docassemble/base/data/sources/base-words.yml b/docassemble_base/docassemble/base/data/sources/base-words.yml
index c8811537b..2292022c9 100644
--- a/docassemble_base/docassemble/base/data/sources/base-words.yml
+++ b/docassemble_base/docassemble/base/data/sources/base-words.yml
@@ -309,10 +309,11 @@
 "Exit": Null
 "Failed to load report": Null
 "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
 "File format not understood: ": Null
 "File name": Null
@@ -1029,10 +1030,11 @@
 "There are unsaved changes.  Are you sure you wish to leave this page?": Null
 "The regular expression you provided could not be parsed.": Null
 "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
 "There was a problem reading the YAML file you uploaded.  Are you sure it is a YAML file?  File was not saved.": Null
 "There was a problem sending the text message.": Null
diff --git a/docassemble_base/docassemble/base/error.py b/docassemble_base/docassemble/base/error.py
index 625b9ed64..4452dc162 100644
--- a/docassemble_base/docassemble/base/error.py
+++ b/docassemble_base/docassemble/base/error.py
@@ -25,10 +25,13 @@ class DAError(Exception):
 
     def __str__(self):
         return str(self.value)
 
 
+class DASourceError(DAError):
+    pass
+
 class DANotFoundError(Exception):
     pass
 
 
 class DAInvalidFilename(Exception):
diff --git a/docassemble_base/docassemble/base/pandoc.py b/docassemble_base/docassemble/base/pandoc.py
index ce050c291..6aabb00fe 100644
--- a/docassemble_base/docassemble/base/pandoc.py
+++ b/docassemble_base/docassemble/base/pandoc.py
@@ -8,20 +8,20 @@ import shutil
 import re
 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
 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')
 
 
@@ -800,17 +800,35 @@ def concatenate_files(path_list, pdfa=False, password=None, owner_password=None)
             new_path_list.append(new_pdf_file.name)
         elif mimetype == 'application/pdf':
             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)
     if password or owner_password:
         pdf_encrypt(pdf_file.name, password, owner_password)
diff --git a/docassemble_base/docassemble/base/parse.py b/docassemble_base/docassemble/base/parse.py
index f306ce926..086571875 100644
--- a/docassemble_base/docassemble/base/parse.py
+++ b/docassemble_base/docassemble/base/parse.py
@@ -47,11 +47,11 @@ import qrcode
 import qrcode.image.svg
 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
 from docassemble.base.logger import logmessage
 from docassemble.base.pandoc import MyPandoc
@@ -2061,13 +2061,13 @@ class Question:
             return f"{self.package}, id:{data.get('id')}"
         return data.get("id") or ""
 
     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
         should_append = True
         if 'register_target' in kwargs:
@@ -2131,26 +2131,26 @@ class Question:
         num_directives = 0
         for directive in ('yesno', 'noyes', 'yesnomaybe', 'noyesmaybe', 'fields', 'buttons', 'choices', 'dropdown', 'combobox', 'signature', 'review'):
             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'):
                     logmessage("Ignoring unknown dictionary key '" + key + "'." + self.idebug(data))
         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']
             if 'progress can go backwards' in data['features'] and isinstance(data['features']['progress can go backwards'], bool):
                 self.interview.options['strict progress'] = data['features']['progress can go backwards']
@@ -2158,11 +2158,11 @@ class Question:
                 self.interview.show_progress_bar_percentage = data['features']['show progress bar percentage']
             if 'progress bar method' in data['features'] and isinstance(data['features']['progress bar method'], str):
                 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']
             if 'question help button' in data['features'] and isinstance(data['features']['question help button'], bool):
                 self.interview.question_help_button = data['features']['question help button']
@@ -2193,10 +2193,12 @@ class Question:
                 self.interview.loop_limit = data['features']['loop limit']
             if 'recursion limit' in data['features']:
                 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']:
                 self.interview.bootstrap_theme = data['features']['bootstrap theme']
                 self.interview.bootstrap_theme_package = self.package
@@ -2229,36 +2231,36 @@ class Question:
                     for item in data['features']['custom datatypes to load']:
                         if isinstance(item, str) and item not in standard_types and item in docassemble.base.functions.custom_types:
                             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']
             for key in ('javascript', 'css'):
                 if key in data['features']:
                     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:
                         if key not in self.interview.external_files:
                             self.interview.external_files[key] = []
                         self.interview.external_files[key].append((self.from_source.get_package(), the_file))
             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']
         if 'scan for variables' in data:
             if data['scan for variables']:
@@ -2270,38 +2272,38 @@ class Question:
                 self.fields_used.add(data['only sets'])
             elif isinstance(data['only sets'], list):
                 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)
                     the_package = self.from_source.get_package()
                     if the_package is not None:
                         item = self.from_source.get_package() + ':data/sources/' + item
                     tr_todo.append(item)
                 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"):
                     the_xlsx_file = docassemble.base.functions.package_data_filename(item)
                     if not os.path.isfile(the_xlsx_file):
@@ -2385,54 +2387,54 @@ class Question:
             self.from_source.set_language(data['default language'])
         if 'on change' in data:
             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'))
                 self.find_fields_in(val)
         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)
                 the_package = self.from_source.get_package()
                 if the_package is not None:
                     new_storage = self.from_source.get_package() + ':data/sources/' + new_storage
                 self.interview.set_ml_store(new_storage)
             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:
             self.language = self.from_source.get_language()
         if 'prevent going back' in data and data['prevent going back']:
@@ -2446,17 +2448,17 @@ class Question:
             self.back_button = None
         if 'allowed to set' in data:
             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'])
         if 'disable continue button' in data and 'question' in data:
             self.disable_continue_button = compile(data['disable continue button'], '<disable continue button>', 'eval')
@@ -2467,28 +2469,28 @@ class Question:
                 usedefs = data['usedefs']
             else:
                 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)
         if 'resume button color' in data:
             self.continuecolor = TextObject(definitions + str(data['resume button color']), question=self)
@@ -2496,16 +2498,16 @@ class Question:
             self.backbuttonlabel = TextObject(definitions + str(data['back button label']), question=self)
         if 'corner back button label' in data:
             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):
                 if 'enable' in data['list collect']:
                     self.list_collect = compile(str(data['list collect']['enable']), '<list collect code>', 'eval')
@@ -2520,18 +2522,18 @@ class Question:
                 if 'allow delete' in data['list collect']:
                     self.list_collect_allow_delete = compile(str(data['list collect']['allow delete']), '<list collect allow delete code>', 'eval')
                 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
             elif data['mandatory'] in (False, None):
                 self.is_mandatory = False
@@ -2550,11 +2552,11 @@ class Question:
             should_append = False
             if not isinstance(data['attachment options'], list):
                 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':
                         if 'initial_yaml' not in self.interview.attachment_options:
                             self.interview.attachment_options['initial_yaml'] = []
@@ -2562,45 +2564,45 @@ class Question:
                             the_list = value
                         else:
                             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:
                             self.interview.attachment_options['additional_yaml'] = []
                         if isinstance(value, list):
                             the_list = value
                         else:
                             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
                 self.initial_code = None
             elif data['initial'] in (False, None):
@@ -2639,14 +2641,14 @@ class Question:
                         if self.scan_for_variables:
                             self.fields_used.add(key)
                         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:
                 self.other_fields_used.add(data['variable name'].strip())
             if 'use objects' in data:
@@ -2667,11 +2669,11 @@ class Question:
                 self.gathered = True
             self.question_type = 'data'
             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:
                 self.other_fields_used.add(data['variable name'])
             if 'use objects' in data:
@@ -2693,11 +2695,11 @@ class Question:
             self.question_type = 'data_from_code'
             self.fields.append(Field({'saveas': data['variable name'], 'type': 'data_from_code', 'data': self.recursive_data_from_code(data['data from code'], objects=self.use_objects)}))
         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']:
                 if isinstance(item, dict):
                     for key in item:
@@ -2705,118 +2707,118 @@ class Question:
                         if self.scan_for_variables:
                             self.fields_used.add(key)
                         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
             self.interview.ids_in_use.add(self.id)
             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):
                 supersedes_list = [str(data['supersedes'])]
             else:
                 supersedes_list = [str(x) for x in data['supersedes']]
             self.interview.id_orderings.append({'type': "supersedes", 'question': self, 'supersedes': supersedes_list})
         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]:
                 data[key] = {'unspecified': data[key]}
             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
                 if 'images' in image_set:
                     if isinstance(image_set['images'], list):
                         image_list = image_set['images']
                     elif isinstance(image_set['images'], dict):
                         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)}
                         else:
                             the_image = image
                         for the_key, value in the_image.items():
                             self.interview.images[the_key] = PackageImage(filename=value, attribution=attribution, setname=setname, package=self.package)
         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:
                 if isinstance(data['mako'], str):
                     list_of_defs = [data['mako']]
                 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 = []
             if 'label' in data['interview help']:
                 data['interview help']['label'] = str(data['interview help']['label'])
@@ -2826,70 +2828,70 @@ class Question:
                 else:
                     the_list = data['interview help']['audio']
                 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):
                     the_list = [data['interview help']['video']]
                 else:
                     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
             if 'heading' in data['interview help']:
                 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:
                 self.interview.helptext[self.language] = []
             self.interview.helptext[self.language].append({'content': help_content, 'heading': help_heading, 'audiovideo': audiovideo, 'label': help_label, 'from': 'interview'})
         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():
                 if content is None:
                     if key in self.interview.default_screen_parts[self.language]:
                         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
             # self.is_generic_list = False
             self.generic_object = data['generic object']
@@ -2902,20 +2904,20 @@ class Question:
         if 'comment' in data and len(data) == 1:
             should_append = False
         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
                 data['metadata']['_origin_package'] = self.from_source.get_package()
                 if 'default language' in data['metadata']:
                     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']]
             if isinstance(data['modules'], list):
                 if 'docassemble.base.util' in data['modules'] or 'docassemble.base.legal' in data['modules']:
@@ -2924,37 +2926,37 @@ class Question:
                 # else:
                 #     logmessage("not setting imports_util to true")
                 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):
                 data['reset'] = [data['reset']]
             if isinstance(data['reset'], list):
                 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']]
             if isinstance(data['imports'], list):
                 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:
                     termitems = termitem.items()
                 for term, definition in termitems:
@@ -2968,16 +2970,16 @@ class Question:
                         re_dict[lang] = re.compile(r"{(?i)(%s)(\|[^\}]*)?}" % (re.sub(r'\s', '\\\s+', lower_other),), re.IGNORECASE | re.DOTALL)  # noqa: W605
                         alt_terms[lang] = tr_tuple[0]
                     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:
                     termitems = termitem.items()
                 for term, definition in termitems:
@@ -3013,11 +3015,11 @@ class Question:
                                 if tr_tuple[0] not in self.interview.terms[lang]:
                                     if lang in definition_textobject.other_lang:
                                         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())
                     term_textobject = TextObject(str(lower_term), question=self)
                     definition_textobject = TextObject(str(data['terms'][term]), question=self)
@@ -3028,11 +3030,11 @@ class Question:
                         if tr_tuple[0] not in self.interview.terms[lang]:
                             if lang in definition_textobject.other_lang:
                                 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:
                 self.interview.autoterms[self.language] = {}
             if isinstance(data['auto terms'], list):
@@ -3053,11 +3055,11 @@ class Question:
                                 if tr_tuple[0] not in self.interview.autoterms[lang]:
                                     if lang in definition_textobject.other_lang:
                                         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())
                     term_textobject = TextObject(str(lower_term), question=self)
                     definition_textobject = TextObject(str(data['auto terms'][term]), question=self)
@@ -3068,30 +3070,30 @@ class Question:
                         if tr_tuple[0] not in self.interview.autoterms[lang]:
                             if lang in definition_textobject.other_lang:
                                 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
             if isinstance(data['default role'], str):
                 self.interview.default_role = [data['default role']]
             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:
                     self.role.append(data['role'])
             elif isinstance(data['role'], list):
                 for rolename in data['role']:
                     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:
             should_append = False
             if isinstance(data['include'], str):
@@ -3107,23 +3109,23 @@ class Question:
                                 new_source = interview_source_from_string('docassemble.base:data/questions/' + re.sub(r'^data/questions/', '', questionPath), interview_source=self.interview.source, parent_source=self.from_source)
                                 if new_source is None:
                                     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:
                         target = '_blank'
                     elif target is False:
@@ -3139,27 +3141,27 @@ class Question:
                     placement = item.get('placement', None)
                     css_class = item.get('css class', None)
                     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:
                         button['target'] = TextObject(definitions + target, question=self)
                     else:
@@ -3181,11 +3183,11 @@ class Question:
                     else:
                         button['forget_prior'] = False
                     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:
                             button['arguments'][key] = val
                     self.action_buttons.append(button)
@@ -3196,14 +3198,14 @@ class Question:
             elif isinstance(data['if'], list):
                 self.condition = [compile(x, '<if code>', 'eval') for x in data['if']]
                 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:
             if isinstance(data['require'], list):
                 self.question_type = 'require'
@@ -3216,15 +3218,15 @@ class Question:
                     raise
                 if 'orelse' in data:
                     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:
             self.attachments = self.process_attachment_list(data['attachments'])
         elif 'attachment code' in data:
@@ -3258,11 +3260,11 @@ class Question:
         #             if rolename not in self.role:
         #                 self.role.append(rolename)
         #     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
             else:
                 try:
@@ -3336,15 +3338,15 @@ class Question:
                 self.content_type = TextObject('text/plain; charset=utf-8')
             if 'response code' in data:
                 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)
         if 'subquestion' in data:
             self.subcontent = TextObject(definitions + str(data['subquestion']), question=self)
@@ -3364,11 +3366,11 @@ class Question:
                             the_list = [value]
                         else:
                             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:
                                 self.audiovideo['help'] = []
                             self.audiovideo['help'].append({'text': TextObject(definitions + str(list_item.strip()), question=self), 'package': self.package, 'type': 'audio'})
@@ -3377,30 +3379,30 @@ class Question:
                             the_list = [value]
                         else:
                             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:
                                 self.audiovideo['help'] = []
                             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)
         if 'audio' in data:
             if not isinstance(data['audio'], list):
                 the_list = [data['audio']]
             else:
                 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:
                     self.audiovideo['question'] = []
                 self.audiovideo['question'].append({'text': TextObject(definitions + str(list_item.strip()), question=self), 'package': self.package, 'type': 'audio'})
@@ -3409,11 +3411,11 @@ class Question:
                 the_list = [data['video']]
             else:
                 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:
                     self.audiovideo['question'] = []
                 self.audiovideo['question'].append({'text': TextObject(definitions + str(list_item.strip()), question=self), 'package': self.package, 'type': 'video'})
@@ -3453,11 +3455,11 @@ class Question:
             if self.scan_for_variables:
                 self.fields_used.add(data['signature'])
             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:
             self.undertext = TextObject(definitions + str(data['under']), question=self)
         if 'pre' in data:
@@ -3467,43 +3469,43 @@ class Question:
         if 'right' in data:
             self.righttext = TextObject(definitions + str(data['right']), question=self)
         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'])
             else:
                 self.other_fields_used.add(data['yesno'])
             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'])
             else:
                 self.other_fields_used.add(data['noyes'])
             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'])
             else:
                 self.other_fields_used.add(data['yesnomaybe'])
             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'])
             else:
                 self.other_fields_used.add(data['noyesmaybe'])
@@ -3513,33 +3515,33 @@ class Question:
                 self.fields_used.add(data['sets'])
             elif isinstance(data['sets'], list):
                 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):
                 self.fields_used.add(data['event'])
             elif isinstance(data['event'], list):
                 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
             shuffle = bool('shuffle' in data and data['shuffle'])
             if 'choices' in data or 'dropdown' in data or 'combobox' in data:
@@ -3563,15 +3565,15 @@ class Question:
                 if has_code:
                     field_data['has_code'] = True
                 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:
                     self.fields_used.add(data['field'])
                 else:
@@ -3595,11 +3597,11 @@ class Question:
                 field_data['extras']['continue button field'] = data['continue button field']
             self.fields.append(Field(field_data))
             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:
                 self.other_fields_used.add(data['continue button field'])
             if 'review' in data:
@@ -3612,36 +3614,36 @@ class Question:
             if isinstance(data['need'], (str, dict)):
                 need_list = [data['need']]
             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))
                     for x in pre_need_list:
                         self.find_fields_in(x)
@@ -3668,22 +3670,22 @@ class Question:
         else:
             depends_list = []
         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 = []
             column = []
             is_editable = False
@@ -3695,13 +3697,13 @@ class Question:
             else:
                 # word('n/a')
                 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()
                 else:
                     for key, val in col.items():
@@ -3717,32 +3719,32 @@ class Question:
             reorder = 'allow reordering' in data and data['allow reordering'] is not False
             if 'edit' in data and data['edit'] is not False:
                 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'
                 if 'confirm' in data and data['confirm']:
                     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'))
                 else:
                     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:
                         header.append(TextObject(definitions + str(data['edit header']), question=self))
                 else:
@@ -3750,21 +3752,21 @@ class Question:
             elif ('delete buttons' in data and data['delete buttons']) or reorder:
                 is_editable = True
                 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'
                 if 'delete buttons' in data and data['delete buttons']:
                     column.append(compile('(' + data['rows'] + ').item_actions(row_item, row_index, edit=False' + keyword_args + ', reorder=' + repr(reorder) + ', ensure_complete=' + repr(ensure_complete) + ')', '<delete button code>', 'eval'))
                 else:
                     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:
                         header.append(TextObject(definitions + str(data['edit header']), question=self))
                 else:
@@ -3793,31 +3795,31 @@ class Question:
                     self.compute = compile(data['content file']['code'], '<content file code>', 'eval')
                     self.sourcecode = data['content file']['code']
                     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:
                 self.other_fields_used.add(data['template'])
             field_data = {'saveas': data['template']}
@@ -3846,14 +3848,14 @@ class Question:
                 except:
                     logmessage("Question: compile error in code:\n" + str(data['code']) + "\n" + str(sys.exc_info()[0]))
                     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:
                         if self.generic_object not in self.interview.reconsider_generic:
                             self.interview.reconsider_generic[self.generic_object] = set()
@@ -3864,55 +3866,55 @@ class Question:
                 if isinstance(data['reconsider'], str):
                     fields = [data['reconsider']]
                 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:
             if isinstance(data['undefine'], str):
                 fields = [data['undefine']]
             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:
                 self.other_fields_used.add(data['continue button field'])
             self.fields_saveas = data['continue button field']
         if 'fields' in data:
             self.question_type = 'fields'
             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
                 if field.get('input type', None) == 'hidden':
                     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']
                 if 'datatype' in field:
                     if field['datatype'] in ('radio', 'combobox', 'pulldown', 'ajax'):
@@ -3926,35 +3928,35 @@ class Question:
                         field['datatype'] = 'text'
                     if field['datatype'] == 'hidden':
                         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'])
                     field_info['extras'] = {'fields_code': compile(field['code'], '<fields code>', 'eval')}
                     self.fields.append(Field(field_info))
                     field_number += 1
                     if 'current_field' in docassemble.base.functions.this_thread.misc:
                         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
                     if custom_data_type:
                         if key in docassemble.base.functions.custom_types[field['datatype']]['parameters']:
@@ -3994,15 +3996,15 @@ class Question:
                             else:
                                 field_info['extras']['ml_train'] = {'compute': compile(field[key], '<keep for training code>', 'eval'), 'sourcecode': field[key]}
                                 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]}
                         self.find_fields_in(field[key])
                     elif key == 'rows' and (('input type' in field and field['input type'] == 'area') or ('datatype' in field and field['datatype'] in ('multiselect', 'object_multiselect'))):
@@ -4025,32 +4027,32 @@ class Question:
                         field_info['extras']['file css class'] = TextObject(DO_NOT_TRANSLATE + str(field[key]), question=self)
                     elif key == 'allow privileges' 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):
-                                    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]]
                         elif isinstance(field[key], dict) and len(field[key]) == 1 and 'code' in field[key]:
                             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]]
                         elif isinstance(field[key], dict) and len(field[key]) == 1 and 'code' in field[key]:
                             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]
                         else:
                             field_info['persistent'] = {'compute': compile(field[key], '<persistent code>', 'eval'), 'sourcecode': field[key]}
@@ -4084,11 +4086,11 @@ class Question:
                         else:
                             field_info['extras']['disabled'] = {'compute': compile(field[key], '<disabled code>', 'eval'), 'sourcecode': field[key]}
                             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
                         else:
                             js_info['sign'] = False
@@ -4098,11 +4100,11 @@ class Question:
                         if 'extras' not in field_info:
                             field_info['extras'] = {}
                         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
                         else:
                             js_info['sign'] = False
@@ -4117,20 +4119,20 @@ class Question:
                             if key != other_key:
                                 if other_key in field:
                                     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)
                                 else:
                                     field_info['extras']['show_if_val'] = TextObject(str(field[key]['is']))
@@ -4138,18 +4140,18 @@ class Question:
                             if 'code' in field[key]:
                                 field_info['showif_code'] = compile(field[key]['code'], '<show if code>', 'eval')
                                 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:
                                 exclusive = True
                             if key == 'show if':
@@ -4171,20 +4173,20 @@ class Question:
                             if key != other_key:
                                 if other_key in field:
                                     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)
                                 else:
                                     field_info['extras']['show_if_val'] = TextObject(str(field[key]['is']))
@@ -4192,18 +4194,18 @@ class Question:
                             if 'code' in field[key]:
                                 field_info['showif_code'] = compile(field[key]['code'], '<show if code>', 'eval')
                                 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:
                                 exclusive = True
                             if key == 'enable if':
@@ -4239,32 +4241,32 @@ class Question:
                                     field_info[key] = field[key]
                                 if 'datatype' not in field and 'code' not in field and 'choices' not in field:
                                     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]
                         if field[key] in ('yesno', 'yesnowide', 'noyes', 'noyeswide') and 'required' not in field_info:
                             field_info['required'] = False
                         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'):
                             field_info['boolean'] = -1
                         elif field[key] == 'yesnomaybe':
@@ -4276,11 +4278,11 @@ class Question:
                         field_info['choicetype'] = 'compute'
                         field_info['selections'] = {'compute': compile(field[key], '<choices code>', 'eval'), 'sourcecode': field[key]}
                         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'])
                             else:
                                 field_info['selections']['exclude'] = []
@@ -4292,11 +4294,11 @@ class Question:
                             field_info['address_autocomplete'] = compile(field[key], '<address autocomplete expression>', 'eval')
                             self.find_fields_in(field[key])
                         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':
                         if isinstance(field[key], str):
                             field_info['label_above_field'] = compile(field[key], '<label above field expression>', 'eval')
@@ -4306,81 +4308,81 @@ class Question:
                     elif key == 'grid':
                         field_info[key] = {}
                         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):
                                     field_info[key][subkey] = compile(field[key][subkey], '<' + key + ' ' + subkey + ' expression>', 'eval')
                                     self.find_fields_in(field[key][subkey])
                                 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):
                                     field_info[key][subkey] = compile(field[key][subkey], '<' + key + ' ' + subkey + ' expression>', 'eval')
                                     self.find_fields_in(field[key][subkey])
                                 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':
                         field_info[key] = {}
                         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':
                         if isinstance(field[key], str):
                             field_info['floating_label'] = compile(field[key], '<floating label expression>', 'eval')
                             self.find_fields_in(field[key])
                         else:
                             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]
                     elif key == 'exclude':
                         pass
                     elif key == 'choices':
                         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'
                             field_info['selections'] = {'values': self.process_selections_manual(field[key])}
                             if 'datatype' not in field:
@@ -4391,11 +4393,11 @@ class Question:
                                         manual_keys.add(item['key'].original_text)
                                 else:
                                     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')]
                             else:
                                 field_info['selections']['exclude'] = []
@@ -4434,30 +4436,30 @@ class Question:
                             field_info['nota'] = field[key]
                         else:
                             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'):
                         if 'aota' not in field_info:
                             field_info['aota'] = False
@@ -4465,27 +4467,27 @@ class Question:
                             field_info['nota'] = True
                     if field_info['type'] == 'object_radio' and 'nota' not in field_info:
                         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:
                             exclude_list = [x.strip() for x in field['exclude']]
                         if len(exclude_list) > 0:
                             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:
                             default_list = field['default']
                     else:
@@ -4504,11 +4506,11 @@ class Question:
                     source_code = "docassemble_base_util_selections(" + ", ".join(select_list) + additional_parameters + ")"
                     # logmessage("source_code is " + source_code)
                     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'):
                             if self.scan_for_variables:
                                 self.fields_used.add(field_info['saveas'])
@@ -4531,11 +4533,11 @@ class Question:
                             if 'extras' in field_info and 'ml_group' in field_info['extras']:
                                 self.interview.mlfields[field_info['saveas']]['ml_group'] = field_info['extras']['ml_group']
                             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'])
                                 else:
                                     self.other_fields_used.add(field_info['saveas'])
@@ -4552,50 +4554,50 @@ class Question:
                     else:
                         if self.scan_for_variables:
                             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:
                         field_info['type'] = 'raw html'
                     else:
                         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']
                 else:
                     tabular_class = 'table table-borderless'
                 self.tabular = TextObject(definitions + tabular_class, question=self)
             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':
                         continue
                     if key == 'help':
                         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)
                             field_info['type'] = 'button'
                     elif key in ('note', 'html', 'raw html'):
@@ -4610,71 +4612,71 @@ class Question:
                         else:
                             field_list = field[key]
                         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)
                         if len(field_list) > 0:
                             if 'saveas_code' not in field_info:
                                 field_info['saveas_code'] = []
                             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:
                             field_list = field[key]
                         field_info['data'] = []
                         for the_saveas in field_list:
                             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': {}})
                                 for command in ('undefine', 'invalidate', 'recompute'):
                                     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':
                                         field_info['data'].append({'action': '_da_invalidate', 'arguments': {'variables': clean_list}})
                                     else:
@@ -4682,25 +4684,25 @@ class Question:
                                     if command == 'recompute':
                                         field_info['data'].append({'action': '_da_compute', 'arguments': {'variables': clean_list}})
                                 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)
                         if 'action' in field:
                             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)
                         if not isinstance(field[key], list):
                             field_list = [field[key]]
@@ -4709,48 +4711,48 @@ class Question:
                         field_info['data'] = []
                         for the_saveas in field_list:
                             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': {}})
                                 for command in ('undefine', 'invalidate', 'recompute'):
                                     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':
                                         field_info['data'].append({'action': '_da_invalidate', 'arguments': {'variables': clean_list}})
                                     else:
@@ -4758,17 +4760,17 @@ class Question:
                                     if command == 'recompute':
                                         field_info['data'].append({'action': '_da_compute', 'arguments': {'variables': clean_list}})
                                 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)
                         if 'action' in field:
                             field_info['action'] = {'action': field['action'], 'arguments': {}}
@@ -4784,28 +4786,28 @@ class Question:
                         else:
                             field_info['action'] = {'action': "_da_force_ask", 'arguments': {'variables': field_info['data']}}
                 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'):
                 self.question_type = 'attachments'
             elif hasattr(self, 'content'):
                 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()
             # self.number = len(self.interview.questions_list) - 1
             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:
             self.number = self.interview.next_block_number()
             if self.name is None:
@@ -5053,11 +5055,11 @@ class Question:
                     options['skip_undefined'] = target['skip undefined']
                 elif isinstance(target['skip undefined'], str):
                     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:
                 options['language'] = target['language']
             if 'name' not in target:
@@ -5073,84 +5075,84 @@ class Question:
                 else:
                     options['redact'] = compile(target['redact'], '<expression>', 'eval')
                     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):
                     target['initial yaml'] = [target['initial yaml']]
                 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):
                     target['additional yaml'] = [target['additional yaml']]
                 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):
                     the_list = [target['usedefs']]
                 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:
                     self.other_fields_used.add(target['variable name'])
             else:
                 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):
                         metadata[key] = TextObject(data, question=self)
                     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']
                     if isinstance(content_file, dict):
                         target['valid formats'] = ['raw']
@@ -5174,24 +5176,24 @@ class Question:
                 if isinstance(target['content file'], dict):
                     if len(target['content file']) == 1 and 'code' in target['content file'] and isinstance(target['content file']['code'], str):
                         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'
             elif 'docx template file' in target:
                 if 'update references' in target:
@@ -5199,11 +5201,11 @@ class Question:
                         options['update_references'] = target['update references']
                     elif isinstance(target['update references'], str):
                         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:
                     target['fields'] = {}
                     if 'code' in target or 'field variables' in target or 'field code' in target or 'raw field variables' in target:
@@ -5214,13 +5216,13 @@ class Question:
                 field_mode = 'manual'
             if 'pdf template file' in target and 'fields' not in target:
                 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']
                     if 'editable' in target:
                         options['editable'] = compile(str(target['editable']), '<editable expression>', 'eval')
@@ -5228,41 +5230,41 @@ class Question:
                     template_type = 'docx'
                     if 'valid formats' in target:
                         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:
                             the_docx_path = template_files[0]
                         else:
@@ -5320,46 +5322,46 @@ class Question:
                         if isinstance(target['code'], str):
                             options['code'] = compile(target['code'], '<expression>', 'eval')
                             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:
                         if 'code dict' not in options:
                             options['code dict'] = {}
                         if not isinstance(target['field code'], list):
                             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)
             if 'valid formats' in target:
                 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:
                 options['password'] = TextObject(target['password'])
             if 'owner password' in target:
@@ -5371,66 +5373,74 @@ class Question:
                     options['persistent'] = target['persistent']
                 elif isinstance(target['persistent'], str):
                     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']
                 elif isinstance(target['private'], str):
                     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')
                 elif isinstance(target['allow privileges'], str):
                     options['allow privileges'] = [target['allow privileges']]
                 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):
                     options['allow users'] = compile(target['allow users']['code'], '<allow users expression>', 'eval')
                 elif isinstance(target['allow users'], (str, int)):
                     options['allow users'] = [target['allow users']]
                 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']
                 elif isinstance(target['pdf/a'], str):
                     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:
                 if isinstance(target['tagged pdf'], bool):
                     options['tagged_pdf'] = target['tagged pdf']
                 elif isinstance(target['tagged pdf'], str):
                     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):
             return {'name': TextObject('Document'), 'filename': TextObject('Document'), 'description': TextObject(''), 'content': TextObject(orig_target, question=self), 'valid_formats': ['*'], 'metadata': metadata, 'variable_name': variable_name, 'orig_variable_name': variable_name, 'options': options, 'raw': False}
         raise DAError("Unknown data type in attachment")
@@ -6109,11 +6119,14 @@ class Question:
                     extra_amount = 0
                 else:
                     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)
                     else:
                         extras['list_message'][list_indexno] = ''
@@ -6356,11 +6369,11 @@ class Question:
                         assumed_objects = set()
                         for field in self.fields:
                             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:
                                     assumed_objects.add(parse_result['bracket_objects'][-1])
                         for var in assumed_objects:
@@ -6537,11 +6550,11 @@ class Question:
                                 real_key = from_safeid(key)
                                 the_string = "_internal['objselections'][" + repr(saveas_to_use) + "][" + repr(key) + "] = " + real_key
                                 # 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'):
                         if 'fields_code' in field.extras:
                             the_question = self.get_question_for_field_with_sub_fields(field, user_dict)
@@ -6689,11 +6702,11 @@ class Question:
                     # m = re.match(r'(.*)\.[^\.]+', from_safeid(field.saveas))
                     # if m and m.group(1) != 'x':
                     #     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:
                         assumed_objects.add(parse_result['bracket_objects'][-1])
             if 'current_field' in docassemble.base.functions.this_thread.misc:
@@ -6792,16 +6805,16 @@ class Question:
                 new_item = {}
                 new_item[key] = value
                 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():
                 if len(the_dict) > 1:
                     if key in ('image', 'css class', 'color', 'help', 'default', 'group', 'label'):
@@ -6848,11 +6861,11 @@ class Question:
                 elif isinstance(value, bool):
                     if not uses_value_label:
                         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)
 
     def mark_as_answered(self, the_user_dict):
         if self.is_mandatory or self.mandatory_code is not None:
@@ -6925,11 +6938,11 @@ class Question:
                             if hasattr(the_file, key):
                                 result[key][doc_format] = getattr(the_file, key)
                         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
             except:
                 pass
@@ -6963,14 +6976,14 @@ class Question:
                             else:
                                 default_export_value = None
                             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()
                         elif (doc_format == 'docx' or (doc_format == 'pdf' and 'docx' not in result['formats_to_use'])) and 'docx_template_file' in attachment['options']:
                             # logmessage("field_data is " + repr(result['field_data']))
@@ -7165,18 +7178,18 @@ class Question:
                         if re.search(r'^https?://', str(the_filename)):
                             with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", delete=False) as temp_template_file:
                                 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()
                 the_content = TextObject(raw_content, question=self)
             else:
@@ -7226,10 +7239,17 @@ class Question:
                     result['convert_to_pdf_a'] = attachment['options']['pdf_a']
                 else:
                     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:
                 result['rendering_font'] = None
             if 'hyperlink_style' in attachment['options']:
@@ -7301,11 +7321,11 @@ class Question:
                         if doc_format == 'docx' or ('docx' not in result['formats_to_use'] and doc_format == 'pdf'):
                             docx_paths = []
                             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]
                             else:
                                 docx_path = docassemble.base.file_docx.concatenate_files(docx_paths)
@@ -7643,11 +7663,11 @@ class Question:
                     result.append({'key': TextObject(str(entry), question=self), 'label': TextObject(str(entry), question=self)})
         elif isinstance(data, dict):
             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
 
 
 def emoji_matcher_insert(obj):
     return (lambda x: docassemble.base.filter.emoji_insert(x.group(1), images=obj.interview.images))
@@ -8306,27 +8326,27 @@ class Interview:
                 try:
                     document = safeyaml.load(source_code)
                 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:
                         question = Question(document, self, source=source, package=source_package, source_code=source_code, line_number=line_number)
                         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'):
                 new_list = [ordering['question'].number]
                 for question_id in ordering['supersedes']:
@@ -8572,25 +8592,25 @@ class Interview:
         try:
             while True:
                 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']))
                     if interview_status.current_info['action'] in ('_da_list_remove', '_da_list_add', '_da_list_complete'):
                         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")
                 try:
                     if not self.imports_util:
@@ -8719,11 +8739,11 @@ class Interview:
                                         continue
                                     if hasattr(the_question, 'content'):
                                         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':
                                     user_dict['_internal']['answered'].add(question.name)
                                 else:
@@ -8774,11 +8794,11 @@ class Interview:
                                 del exc
                                 del tb
                                 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
                         follow_mc = True
                         missingVariable = extract_missing_name(the_exception)
@@ -8943,11 +8963,11 @@ class Interview:
                         del docassemble.base.functions.this_thread.misc['pending_error']
                     # logmessage("Regular attributeerror")
                     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()
                     break
                 except CodeExecute as code_error:
@@ -8965,24 +8985,24 @@ class Interview:
                         the_question = question
                     except:
                         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()
                     the_question = None
                     try:
                         the_question = question
                     except:
                         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.')
         except Exception as the_error:
             # logmessage("Untrapped exception")
@@ -9019,17 +9039,17 @@ class Interview:
         follow_mc = kwargs.get('follow_mc', True)
         seeking = kwargs.get('seeking', [])
         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
         totry = []
         variants = []
@@ -9076,11 +9096,11 @@ class Interview:
         num_cycles = 0
         missing_var = "_unknown"
         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")
             try:
                 for the_question, is_generic, the_x, iterators, missing_var, generic_object in questions_to_try:
@@ -9368,18 +9388,18 @@ class Interview:
                                 if re.search(r'^https?://', str(the_filename)):
                                     with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", delete=False) as temp_template_file:
                                         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 = {}
                         if is_generic:
                             if the_x != 'None':
@@ -9528,11 +9548,11 @@ class Interview:
                         interview_status.mark_tentative_as_answered(user_dict)
                         if question.question_type == 'continue':
                             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)
                     if 'action' in docassemble.base.functions.this_thread.current_info and docassemble.base.functions.this_thread.current_info['action'] == origMissingVariable:
                         del docassemble.base.functions.this_thread.current_info['action']
@@ -9599,21 +9619,21 @@ class Interview:
                             del exc
                             del tb
                             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
                     follow_mc = True
                     newMissingVariable = extract_missing_name(the_exception)
                 if newMissingVariable == 'file':
                     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:
                     variable_stack.add(missingVariable)
                 if current_question.question_type != 'objects':
@@ -9782,34 +9802,34 @@ class Interview:
                     # logmessage("returning from running code")
                     docassemble.base.functions.pop_current_variable()
                     # 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")
                 docassemble.base.functions.reset_context()
                 the_question = None
                 try:
                     the_question = question
                 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()
                 the_question = None
                 try:
                     the_question = question
                 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': {}}
             #     if hasattr(qError, 'filename') and qError.filename is not None:
             #         question_data['response filename'] = qError.filename
@@ -9971,11 +9991,11 @@ def process_selections(data, exclude=None):
                 elif hasattr(value, 'instanceName'):
                     result.append({'key': key, 'label': str(value)})
                 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
 
 
 def extract_missing_name(the_error):
     # logmessage("extract_missing_name: string was " + str(string))
@@ -10077,11 +10097,11 @@ def ensure_object_exists(saveas, datatype, the_user_dict, commands=None):
     if already_there:
         # logmessage("ensure object exists: already there")
         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] == '.':
             try:
                 core_key = eval(parse_result['final_parts'][0], the_user_dict)
@@ -10730,22 +10750,22 @@ 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()
         the_env = custom_jinja_env()
         the_xml = docx_template.get_xml()
         the_xml = re.sub(r'<w:p([ >])', r'\n<w:p\1', the_xml)
         the_xml = re.sub(r'({[\%\{].*?[\%\}]})', fix_quotes, the_xml)
         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)
     from docassemble.base.legal import __all__ as legal_all  # pylint: disable=import-outside-toplevel
     for name in legal_all:
diff --git a/docassemble_base/docassemble/base/pdftk.py b/docassemble_base/docassemble/base/pdftk.py
index fc7cb5982..b2c755ff3 100644
--- a/docassemble_base/docassemble/base/pdftk.py
+++ b/docassemble_base/docassemble/base/pdftk.py
@@ -158,11 +158,11 @@ def recursively_add_fields(fields, id_to_page, outfields, prefix='', parent_ft=N
                 outfields.append((name, default, pageno, rect, field_type, export_value))
             else:
                 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:
         data_names = []
     if hidden is None:
@@ -217,11 +217,11 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea
         data_strings = new_data_strings
     data_dict = {}
     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)
         # fdf_file.write(fdf)
         fdf_file.close()
@@ -249,11 +249,14 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea
         subprocess_arguments = [PDFTK_PATH, template, 'fill_form', fdf_file.name, 'output', pdf_file.name] + font_arguments
         # logmessage("Arguments are " + str(subprocess_arguments))
         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)
             result = completed_process.returncode
         except subprocess.TimeoutExpired:
@@ -270,11 +273,10 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea
     else:
         if template_password:
             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
             for the_annot in page.Annots:
                 for field, value in data_dict.items():
@@ -318,10 +320,13 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea
                             opt_list.append(value)
                             annot.Opt = pikepdf.Array(opt_list)
                         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()
     if len(images) > 0:
         fields = {}
diff --git a/docassemble_webapp/docassemble/webapp/develop.py b/docassemble_webapp/docassemble/webapp/develop.py
index 7cf0ae29f..d76f63819 100644
--- a/docassemble_webapp/docassemble/webapp/develop.py
+++ b/docassemble_webapp/docassemble/webapp/develop.py
@@ -1,9 +1,10 @@
 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
 
 
 class NonValidatingSelectField(SelectField):
 
@@ -35,25 +36,25 @@ def validate_package_name(form, field):  # pylint: disable=unused-argument
         raise ValidationError(word('Valid characters are: A-Z, a-z, 0-9'))
 
 
 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'))
 
 
 class ConfigForm(FlaskForm):
     config_content = TextAreaField(word('Configuration YAML'))
@@ -62,11 +63,11 @@ 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'))
     run = SubmitField(word('Save and Run'))
     delete = SubmitField(word('Delete'))
@@ -106,62 +107,62 @@ class PlaygroundFilesForm(FlaskForm):
 
 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'))
     submit = SubmitField(word('Save'))
     delete = SubmitField(word('Delete'))
 
 
 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'))
 
 
 class DeleteProject(FlaskForm):
     submit = SubmitField(word('Delete'))
 
 
 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'))
     module_files = SelectMultipleField(word('Modules'))
     static_files = SelectMultipleField(word('Static files'))
     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'))
     submit = SubmitField(word('Save'))
     download = SubmitField(word('Download'))
@@ -220,11 +221,11 @@ class FunctionFileForm(FlaskForm):
 
 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'))
     delete = SubmitField(word('Delete'))
 
diff --git a/docassemble_webapp/docassemble/webapp/server.py b/docassemble_webapp/docassemble/webapp/server.py
index 19389d818..0f48166cd 100644
--- a/docassemble_webapp/docassemble/webapp/server.py
+++ b/docassemble_webapp/docassemble/webapp/server.py
@@ -47,11 +47,11 @@ from docassemble.base.config import daconfig, hostname, in_celery, in_cron, DEBU
 
 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
 from docassemble.base.generate_key import random_string, random_lower_string, random_alphanumeric, random_digits
 import docassemble.base.interview_cache
@@ -157,11 +157,10 @@ docassemble.base.util.set_knn_machine_learner(docassemble.webapp.machinelearning
 docassemble.base.util.set_machine_learning_entry(docassemble.webapp.machinelearning.MachineLearningEntry)
 docassemble.base.util.set_random_forest_machine_learner(docassemble.webapp.machinelearning.RandomForestMachineLearner)
 docassemble.base.util.set_svm_machine_learner(docassemble.webapp.machinelearning.SVMMachineLearner)
 
 
-
 min_system_version = '1.2.0'
 re._MAXCACHE = 10000
 
 the_method_type = types.FunctionType
 equals_byte = bytes('=', 'utf-8')
@@ -1176,16 +1175,21 @@ def unauthorized():
 def my_default_url(error, endpoint, values):  # pylint: disable=unused-argument
     return url_for('index')
 
 
 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
 
 
 def password_validator(form, field):  # pylint: disable=unused-argument
     password = list(field.data)
@@ -1236,10 +1240,11 @@ lm.login_view = 'custom_login'
 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']:
             args['dispatch'] = k
             del args['i']
@@ -5838,11 +5843,11 @@ def github_unconfigure():
 def github_oauth_callback():
     if not app.config['ENABLE_PLAYGROUND']:
         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
     elif 'github_next' not in session:
         logmessage('github_oauth_callback: next not in session')
@@ -5851,18 +5856,18 @@ def github_oauth_callback():
         github_next = json.loads(session['github_next'])
         del session['github_next']
         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)
     flow = get_github_flow()
     credentials = flow.step2_exchange(request.args['code'])
@@ -8263,11 +8268,11 @@ def index(action_argument=None, refer=None):
                     the_error_message = word("Please enter a valid value.")
                 if isinstance(validation_error, DAValidationError) and isinstance(validation_error.field, str):
                     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
                     if the_field in key_to_orig_key:
                         field_error[key_to_orig_key[the_field]] = the_error_message
@@ -10913,11 +10918,11 @@ def index(action_argument=None, refer=None):
       }
       function daDisableIfNotHidden(query, value){
         $(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();
               }
               else {
                 daComboBoxes[$(this).attr('id')].enable();
@@ -13999,11 +14004,11 @@ def observer():
       }
       function daDisableIfNotHidden(query, value){
         $(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();
               }
               else {
                 daComboBoxes[$(this).attr('id')].enable();
@@ -23041,11 +23046,18 @@ def server_error(the_error):
     if hasattr(the_error, 'interview') and the_error.interview.debug and hasattr(the_error, 'interview_status'):
         the_history = get_history(the_error.interview, the_error.interview_status)
     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)
     elif isinstance(the_error, TemplateError):
         errmess = str(the_error)
@@ -23071,11 +23083,14 @@ def server_error(the_error):
             the_trace = traceback.format_exc()
         if hasattr(docassemble.base.functions.this_thread, 'misc') and 'current_field' in docassemble.base.functions.this_thread.misc:
             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
     if isinstance(the_error, DANotFoundError):
         error_code = 404
@@ -23294,11 +23309,11 @@ def server_error(the_error):
         except:
             pass
         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:
         the_template = 'pages/404.html'
     else:
@@ -26998,10 +27013,11 @@ def invite_user(email_address, privilege=None, send=True):
             db.session.commit()
             raise DAError("Invitation email failed to send")
         return None
     return accept_invite_link
 
+
 @app.route('/api/user_invite', methods=['POST'])
 @csrf.exempt
 @cross_origin(origins='*', methods=['POST', 'HEAD'], automatic_options=True)
 def api_invite_user():
     if not api_verify(roles=['admin'], permissions=['create_user']):
diff --git a/docassemble_webapp/docassemble/webapp/users/forms.py b/docassemble_webapp/docassemble/webapp/users/forms.py
index c3f3ef915..981b24104 100644
--- a/docassemble_webapp/docassemble/webapp/users/forms.py
+++ b/docassemble_webapp/docassemble/webapp/users/forms.py
@@ -3,20 +3,21 @@ import email.utils
 from docassemble_flask_user.forms import RegisterForm, LoginForm, password_validator, unique_email_validator
 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
 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:
     if 'ldap login' not in daconfig:
         daconfig['ldap login'] = {}
@@ -205,25 +206,26 @@ def da_registration_restrict_validator(form, field):  # pylint: disable=unused-a
     form.email.errors = tuple(errors)
     return False
 
 
 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
     if len(field.data) != 2:
         raise ValidationError(word('Must be a two-letter code'))
@@ -234,28 +236,28 @@ class NewPrivilegeForm(FlaskForm):
         DataRequired(word('Name of new privilege is required'))])
     submit = SubmitField(word('Add'))
 
 
 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'))
     cancel = SubmitField(word('Cancel'))
 
 
 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'))
 
     def validate(self, user_id, admin_id):  # pylint: disable=arguments-differ
@@ -297,15 +299,15 @@ class PhoneUserProfileForm(UserProfileForm):
                 existing_user = db.session.execute(select(UserModel).filter_by(email=self.email.data, active=True)).scalar()
                 if existing_user is not None and existing_user.id != current_user.id:
                     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'))
 
 
 class MyInviteForm(FlaskForm):
 
@@ -332,25 +334,25 @@ class MyInviteForm(FlaskForm):
 
 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
         result = True
         if BAN_IP_ADDRESSES:
@@ -402,11 +404,11 @@ class MFAChooseForm(FlaskForm):
     sms = SubmitField(word('SMS'))
     cancel = SubmitField(word('Cancel'))
 
 
 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'))
 
 
 class MFAVerifySMSSetupForm(FlaskForm):
     verification_code = StringField(word('Verification code'))
@@ -414,12 +416,12 @@ 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'))
 
 
 class ManageAccountForm(FlaskForm):
     confirm = StringField(word('Type \"delete my account\" here to confirm that you want to delete your account.'), [validators.AnyOf(LazyArray([word("delete my account")]), message=word('Since you did not type \"delete my account\" I did not delete your account.'))])
diff --git a/docassemble_webapp/docassemble/webapp/validators.py b/docassemble_webapp/docassemble/webapp/validators.py
new file mode 100644
index 000000000..fab3a24cf
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/validators.py
@@ -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'))

Vulnerable code: && unzip -o omw-1.4.zip"
import convertapi
import requests
from pikepdf import Pdf
from docassemble.base.error import DAError, DANotFoundError, MandatoryQuestion, DAErrorNoEndpoint, DAErrorMissingVariable, ForcedNameError, QuestionError, ResponseError, BackgroundResponseError, BackgroundResponseActionError, CommandError, CodeExecute, DAValidationError, ForcedReRun, LazyNameError, DAAttributeError, DAIndexError, DAException, DANameError
            raise DAError("A block must be in the form of a dictionary." + self.idebug(orig_data))
            raise DAError(orig_data['__error__'])
            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 DAError("This block is missing a 'question' directive." + self.idebug(data))
                raise DAError("A features section must be a dictionary." + self.idebug(data))
                    raise DAError("Table width in features must be an integer." + self.idebug(data))
                    raise DAError("progress bar multiplier in features must be between 0 and 1." + self.idebug(data))
                    raise DAError("A features section checkin interval entry must be an integer." + self.idebug(data))
                    raise DAError("A features section checkin interval entry must be at least 1000, if not 0." + self.idebug(data))
                        raise DAError("A features section " + key + " entry must be a list or plain text." + self.idebug(data))
                        raise DAError("A features section " + key + " entry must be plain text." + self.idebug(data))
                        raise DAError("The " + key + " in features did not contain a valid date." + self.idebug(data))
                raise DAError("An only sets phrase must be text or a list." + self.idebug(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 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 DAError("A 'translations' block must be a list" + self.idebug(data))
                    raise DAError("A 'translations' block must be a list of text items" + self.idebug(data))
                    raise DAError("Invalid translations entry '" + item + "'.  A translations entry must refer to a file ending in .xlsx, .xlf, or .xliff." + self.idebug(data))
                    raise DAError("Invalid translations entry: " + item + ".  A translations entry must refer to a data sources file" + self.idebug(data))
                raise DAError("An on change block must be a dictionary." + self.idebug(data))
                raise DAError("An on change block must not contain any other keys." + self.idebug(data))
                    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 DAError("A sections list must be a list." + self.idebug(data))
                raise DAError("A progressive directive can only be used with sections." + self.idebug(data))
                raise DAError("A progressive directive can only be true or false." + self.idebug(data))
                raise DAError("An auto open directive can only be used with sections." + self.idebug(data))
                raise DAError("An auto open directive can only be true or false." + self.idebug(data))
                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 DAError("Invalid machine learning storage entry: " + str(data['machine learning storage']) + self.idebug(data))
                        raise DAError("When allowed to set is a list, it must be a list of text items." + self.idebug(data))
                raise DAError("When allowed to set is not a list, it must be plain text." + self.idebug(data))
                    raise DAError("A usedefs section must consist of a list of strings or a single string." + self.idebug(data))
                    raise DAError('Referred to a non-existent def "' + usedef + '."  All defs must be defined before they are used.' + self.idebug(data))
                raise DAError("You can only set the section from a question." + self.idebug(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 DAError("You cannot set a resume button label if the type of question is not review." + self.idebug(data))
                raise DAError("You cannot set the skip undefined directive if the type of question is not review." + self.idebug(data))
                raise DAError("You cannot set list collect without a fields specifier." + self.idebug(data))
                raise DAError("Invalid data under list collect." + self.idebug(data))
                raise DAError("You cannot use the mandatory modifier and the initial modifier at the same time." + self.idebug(data))
                raise DAError("You cannot use the mandatory modifier on this type of block." + self.idebug(data))
                    raise DAError("An attachment option must a dictionary." + self.idebug(data))
                                raise DAError('An initial yaml file must be a string.' + self.idebug(data))
                                raise DAError('An additional yaml file must be a string.' + self.idebug(data))
                            raise DAError('The template file must be a string.' + self.idebug(data))
                            raise DAError('The rtf template file must be a string.' + self.idebug(data))
                            raise DAError('The docx reference file must be a string.' + self.idebug(data))
                raise DAError("A script section must be plain text." + self.idebug(data))
                raise DAError("A css section must be plain text." + self.idebug(data))
            raise DAError("Only a code block can be marked as initial." + self.idebug(data))
                    raise DAError("An objects section cannot contain a nested list." + self.idebug(data))
                raise DAError("A data block variable name must be plain text." + self.idebug(data))
                raise DAError("A data from code block variable name must be plain text." + self.idebug(data))
                # raise DAError("An objects section must be organized as a list." + self.idebug(data))
                    raise DAError("An objects section cannot contain a nested list." + self.idebug(data))
            #     raise DAError("The id " + str(data['id']) + " is already in use by another block.  Id names must be unique." + self.idebug(data))
                raise DAError("A 'ga id' must refer to text." + self.idebug(data))
                raise DAError("A 'segment id' must refer to text." + self.idebug(data))
                raise DAError("A 'segment' must refer to a dictionary." + self.idebug(data))
                    raise DAError("An 'id' under 'segment' must refer to text." + self.idebug(data))
                    raise DAError("An 'arguments' under 'segment' must refer to a dictionary." + self.idebug(data))
                        raise DAError("Each item under 'arguments' in a 'segment' must be plain text." + self.idebug(data))
                raise DAError("An 'order' block cannot be combined with another type of block." + self.idebug(data))
                raise DAError("An 'order' block must be a list." + self.idebug(data))
                raise DAError("The '" + key + "' section needs to be a dictionary, not a list or text." + self.idebug(data))
                        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 DAError("An attribution in an 'image set' section cannot be a dictionary or a list." + self.idebug(data))
                            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 DAError("A def name must be a string." + self.idebug(data))
                    raise DAError("A mako template definition must be a string or a list of strings." + self.idebug(data))
                        raise DAError("A mako template definition must be a string." + self.idebug(data))
                raise DAError("An interview help section must not be in the form of a list." + self.idebug(data))
                        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 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 DAError("A heading within an interview help section must be text, not a list or a dictionary." + self.idebug(data))
                    raise DAError("Help content must be text, not a list or a dictionary." + self.idebug(data))
                raise DAError("No content section was found in an interview help section." + self.idebug(data))
                    raise DAError("Help label must be text, not a list or a dictionary." + self.idebug(data))
                raise DAError("A default screen parts block must be in the form of a dictionary." + self.idebug(data))
                        raise DAError("A default screen parts block must be a dictionary of text keys and text values." + self.idebug(data))
                raise DAError("A default validation messages block must be in the form of a dictionary." + self.idebug(data))
                    raise DAError("A validation messages block must be a dictionary of text keys and text values." + self.idebug(data))
                    raise DAError("A metadata directive cannot be mixed with other directives." + self.idebug(data))
                raise DAError("A metadata section must be organized as a dictionary." + self.idebug(data))
                raise DAError("A modules section must be organized as a list." + self.idebug(data))
                raise DAError("A reset section must be organized as a list." + self.idebug(data))
                raise DAError("An imports section must be organized as a list." + self.idebug(data))
                raise DAError("Terms must be organized as a dictionary or a list." + self.idebug(data))
                    raise DAError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data))
                raise DAError("Terms must be organized as a dictionary or a list." + self.idebug(data))
                    raise DAError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data))
                        raise DAError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data))
                raise DAError("A terms section must be organized as a dictionary or a list." + self.idebug(data))
                        raise DAError("An auto terms section organized as a list must be a list of dictionary items." + self.idebug(data))
                raise DAError("An auto terms section must be organized as a dictionary or a list." + self.idebug(data))
                raise DAError("A default role must be a list or a string." + self.idebug(data))
                raise DAError("The role of a question must be a string or a list." + self.idebug(data))
                        raise DAError('An include section could not find the file ' + str(questionPath) + '.' + self.idebug(data))
                raise DAError("An include section must be organized as a list." + self.idebug(data))
                    raise DAError("An action buttons specifier must be a list." + self.idebug(data))
                        raise DAError("An action buttons item must be a dictionary." + self.idebug(data))
                        raise DAError("An action buttons item must contain an action in plain text." + self.idebug(data))
                        raise DAError("The new window specifier in an action buttons item must refer to True or plain text." + self.idebug(data))
                        raise DAError("The arguments specifier in an action buttons item must refer to a dictionary." + self.idebug(data))
                        raise DAError("An action buttons item must contain a label in plain text." + self.idebug(data))
                        raise DAError("The color specifier in an action buttons item must refer to plain text." + self.idebug(data))
                        raise DAError("The icon specifier in an action buttons item must refer to plain text." + self.idebug(data))
                        raise DAError("The placement specifier in an action buttons item must refer to plain text." + self.idebug(data))
                        raise DAError("The css classifier specifier in an action buttons item must refer to plain text." + self.idebug(data))
                        raise DAError("The forget prior specifier in an action buttons item must refer to true or false." + self.idebug(data))
                            raise DAError("The arguments specifier in an action buttons item must refer to plain items." + self.idebug(data))
                raise DAError("An if statement must either be text or a list." + self.idebug(data))
                raise DAError("A validation code statement must be text." + self.idebug(data))
                        raise DAError("The orelse part of a require section must be organized as a dictionary." + self.idebug(data))
                    raise DAError("A require section must have an orelse part." + self.idebug(data))
                raise DAError("A require section must be organized as a list." + self.idebug(data))
        #         raise DAError("A role section must be text or a list." + self.idebug(data))
                raise DAError("A css class can only accompany a question." + self.idebug(data))
                raise DAError("A table css class can only accompany a question." + self.idebug(data))
                                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 DAError("A video declaration in a help block can only contain a text item or a list of text items." + self.idebug(data))
                            raise DAError("A content declaration in a help block can only contain text." + self.idebug(data))
                    raise DAError("An audio declaration can only contain a text item or a list of text items." + self.idebug(data))
                    raise DAError("A video declaration can only contain a text item or a list of text items." + self.idebug(data))
            raise DAError("The required modifier can only be used on a signature block" + self.idebug(data))
                raise DAError("A check in event must be text or a list." + self.idebug(data))
                raise DAError("A yesno must refer to text." + self.idebug(data))
                raise DAError("A noyes must refer to text." + self.idebug(data))
                raise DAError("A yesnomaybe must refer to text." + self.idebug(data))
                raise DAError("A noyesmaybe must refer to text." + self.idebug(data))
                raise DAError("A sets phrase must be text or a list." + self.idebug(data))
                raise DAError("An event phrase must be text or a list." + self.idebug(data))
                    raise DAError("Missing or invalid variable name " + repr(data['field']) + "." + self.idebug(data))
                        raise DAError("Missing or invalid variable name " + repr(data['continue button field']) + "." + self.idebug(data))
                    raise DAError("A validation messages indicator must be a dictionary." + self.idebug(data))
                        raise DAError("A validation messages indicator must be a dictionary of text keys and text values." + self.idebug(data))
                raise DAError("A continue button field must be plain text." + self.idebug(data))
                raise DAError("A need phrase must be text or a list." + self.idebug(data))
                        raise DAError("If 'need' contains a dictionary it can only include keys 'pre' or 'post'." + self.idebug(data))
                            raise DAError("A need post phrase must be text or a list." + self.idebug(data))
                            raise DAError("A need pre phrase must be text or a list." + self.idebug(data))
                    raise DAError("In 'need', the items must be text strings." + self.idebug(data))
                raise DAError("The target of a template must be plain text." + self.idebug(data))
                raise DAError("A target directive can only be used with a template." + self.idebug(data))
                raise DAError("A table definition must have definitions for table, row, and column." + self.idebug(data))
                raise DAError("The row part of a table definition must be plain Python code." + self.idebug(data))
                raise DAError("The column part of a table definition must be a list." + self.idebug(data))
                    raise DAError("The column items in a table definition must be dictionaries." + self.idebug(data))
                    raise DAError("A column item in a table definition cannot be empty." + self.idebug(data))
                        raise DAError("The edit directive must be a list of attributes, or True or False" + self.idebug(data))
                            raise DAError("The edit directive must be a list of attribute names" + self.idebug(data))
                    raise DAError("The edit directive must be a list of attributes, or True or False" + self.idebug(data))
                        raise DAError("The read only directive must be plain text referring to an attribute" + self.idebug(data))
                        raise DAError("The edit header directive must be text" + self.idebug(data))
                        raise DAError("The read only directive must be plain text referring to an attribute" + self.idebug(data))
                        raise DAError("The edit header directive must be text" + self.idebug(data))
                    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 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 DAError('The content file ' + str(data['content file']) + ' is not a markdown file ' + str(file_to_read) + self.idebug(data))
                        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 DAError("A template must designate a single variable expressed as text." + self.idebug(data))
                raise DAError("The content of a template must be expressed as text." + self.idebug(data))
                raise DAError("A code section must be text, not a list or a dictionary." + self.idebug(data))
            #    raise DAError("A reconsider directive must be true or false." + self.idebug(data))
                    raise DAError("A reconsider directive must be true, false, a single variable or a list." + self.idebug(data))
                        raise DAError("A reconsider directive must refer to variable names expressed as text." + self.idebug(data))
                raise DAError("A undefine directive must a single variable or a list." + self.idebug(data))
                    raise DAError("A undefine directive must refer to variable names expressed as text." + self.idebug(data))
                raise DAError("A continue button field must be plain text." + self.idebug(data))
                raise DAError("The fields must be written in the form of a list." + self.idebug(data))
                    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 DAError("Invalid datatype of hidden field." + self.idebug(data))
                        raise DAError("A multiple choice field must refer to a list of choices." + self.idebug(data))
                            raise DAError("An ajax field must have an associated action." + self.idebug(data))
                            raise DAError("An ajax field cannot contain a list of choices except through an action." + self.idebug(data))
                        raise DAError("A multiple choice field must refer to a list of choices." + self.idebug(data))
                    raise DAError("An object labeler can only be used with an object data type")
                    raise DAError("You cannot combine note, html, and/or raw html in a single field." + self.idebug(data))
                            raise DAError("A validation messages indicator must be a dictionary." + self.idebug(data))
                                raise DAError("A validation messages indicator must be a dictionary of text keys and text values." + self.idebug(data))
                                    raise DAError("An allow privileges specifier must be a list of plain text items or code." + self.idebug(data))
                            raise DAError("An allow privileges specifier must be a list of plain text items or code." + self.idebug(data))
                                    raise DAError("An allow users specifier must be a list of integers and plain text items or code." + self.idebug(data))
                            raise DAError("An allow users specifier must be a list of integers and plain text items or code." + self.idebug(data))
                            raise DAError("A js show if or js hide if expression must be a string" + self.idebug(data))
                            raise DAError("A js disable if or js enable if expression must be a string" + self.idebug(data))
                                        raise DAError(key + " cannot be combined with " + other_key)
                                    raise DAError("You cannot mix js show if and non-js show if" + self.idebug(data))
                                    raise DAError("You cannot mix js disable if and non-js show if" + self.idebug(data))
                                raise DAError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data))
                            raise DAError("The keys of '" + key + "' cannot be a list" + self.idebug(data))
                            raise DAError("Invalid variable name in show if/hide if")
                                        raise DAError(key + " cannot be combined with " + other_key)
                                    raise DAError("You cannot mix js show if and non-js disable if" + self.idebug(data))
                                    raise DAError("You cannot mix js disable if and non-js disable if" + self.idebug(data))
                                raise DAError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data))
                            raise DAError("The keys of '" + key + "' cannot be a list" + self.idebug(data))
                            raise DAError("Invalid variable name in " + key)
                            raise DAError("A 'disable others' directive cannot be used with this data type." + self.idebug(data))
                            raise DAError("A 'disable others' directive must be True, False, or a list of variable names." + self.idebug(data))
                            raise DAError("An 'uncheck others' directive must be True, False, or a list of variable names." + self.idebug(data))
                            raise DAError("A 'check others' directive must be True, False, or a list of variable names." + self.idebug(data))
                            raise DAError("If the datatype of a field is 'range', you must provide a min and a max." + self.idebug(data))
                                raise DAError("An exclude entry cannot be a dictionary." + self.idebug(data))
                            raise DAError("address autocomplete must be a Python expression, a dictionary, or a boolean value." + self.idebug(data))
                            raise DAError(key + " is not in the correct format." + self.idebug(data))
                                raise DAError(key + " has an invalid key " + repr(item) + "." + self.idebug(data))
                            raise DAError(key + ' must specify a width.' + self.idebug(data))
                                    raise DAError(key + " " + subkey + " must be a number between 1 and 12, or a Python expression." + self.idebug(data))
                                    raise DAError(key + " " + subkey + " must be True or False, or a Python expression." + self.idebug(data))
                            raise DAError(key + " is not in the correct format." + self.idebug(data))
                                raise DAError(key + " has an invalid key " + repr(item) + "." + self.idebug(data))
                            raise DAError(key + ' must specify a width.' + self.idebug(data))
                            raise DAError(key + " width must be a number between 1 and 12, or a Python expression." + self.idebug(data))
                            raise DAError("An action must be plain text" + self.idebug(data))
                            raise DAError("A trigger at must an integer greater than one" + self.idebug(data))
                                raise DAError("choices is not in appropriate format" + self.idebug(data))
                                raise DAError("An exclude entry cannot be a dictionary." + self.idebug(data))
                            raise DAError("If you use 'field' to indicate a variable in a 'fields' section, you must also include a 'label.'" + self.idebug(data))
                            raise DAError("Fields in a 'field' section must be plain text." + self.idebug(data))
                            raise DAError("Missing or invalid variable name " + repr(field[key]) + "." + self.idebug(data))
                            raise DAError("If you use 'label' to label a field in a 'fields' section, you must also include a 'field.'" + self.idebug(data))
                            raise DAError("Syntax error: field label '" + str(key) + "' overwrites previous label, '" + str(field_info['label'].original_text) + "'" + self.idebug(data))
                            raise DAError("Fields in a 'field' section must be plain text." + self.idebug(data))
                            raise DAError("Missing or invalid variable name " + repr(field[key]) + " for key " + repr(key) + "." + self.idebug(data))
                        raise DAError("You need to have a choices element if you want to set a variable to an object." + self.idebug(data))
                            raise DAError("choices exclude list is not in appropriate format" + self.idebug(data))
                            raise DAError("default list is not in appropriate format" + self.idebug(data))
                        raise DAError("Invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data))
                                    raise DAError("Missing or invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data))
                elif 'note' in field or 'html' or 'raw html' in field:
                    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 DAError("A review block without a continue button field cannot be mandatory." + self.idebug(data))
                raise DAError("The review must be written in the form of a list." + self.idebug(data))
                    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 DAError("In a review block, you cannot mix help text with a button item." + self.idebug(data))  # , css, or script
                            #    raise DAError("Invalid variable name in fields." + self.idebug(data))
                            #    raise DAError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data))
                            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 DAError("The set statement must refer to a list." + self.idebug(data))
                                            raise DAError("A set command must refer to a list of dicts." + self.idebug(data))
                                                raise DAError("A set command must refer to a list of dicts with keys as variable names." + self.idebug(data))
                                                raise DAError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data))
                                        raise DAError("The follow up statement must refer to a list." + self.idebug(data))
                                            raise DAError("Invalid variable name in follow up " + repr(var) + "." + self.idebug(data))
                                            raise DAError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data))
                                        raise DAError("The " + command + " statement must refer to a list." + self.idebug(data))
                                            raise DAError("Invalid variable name " + repr(undef_var) + " in " + command + "." + self.idebug(data))
                                            raise DAError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data))
                                    raise DAError("An arguments directive must refer to a dictionary.  " + repr(data))
                                raise DAError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data))
                                raise DAError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data))
                            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 DAError("The set statement must refer to a list." + self.idebug(data))
                                            raise DAError("A set command must refer to a list of dicts." + self.idebug(data))
                                                raise DAError("A set command must refer to a list of dicts with keys as variable names." + self.idebug(data))
                                                raise DAError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data))
                                        raise DAError("The follow up statement must refer to a list." + self.idebug(data))
                                            raise DAError("Invalid variable name in follow up " + command + "." + self.idebug(data))
                                            raise DAError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data))
                                        raise DAError("The " + command + " statement must refer to a list." + self.idebug(data))
                                            raise DAError("Invalid variable name " + repr(undef_var) + " in fields " + command + "." + self.idebug(data))
                                            raise DAError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data))
                                    raise DAError("An arguments directive must refer to a dictionary.  " + repr(data))
                                raise DAError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data))
                                raise DAError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data))
                    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 DAError("No question type could be determined for this section." + self.idebug(data))
                #     raise DAError("Question ID " + str(self.id) + " results in duplicate question name")
                    raise DAError('Unknown data type in attachment skip undefined.' + self.idebug(target))
                    raise DAError("A checkbox export value must be a string." + self.idebug(target))
                    raise DAError("A decimal places directive must be an integer or string." + self.idebug(target))
                        raise DAError('An initial yaml file must be a string.' + self.idebug(target))
                        raise DAError('An additional yaml file must be a string.' + self.idebug(target))
                    raise DAError('The template file must be a string.' + self.idebug(target))
                    raise DAError('The rtf template file must be a string.' + self.idebug(target))
                    raise DAError('The docx reference file must be a string.' + self.idebug(target))
                    raise DAError('The usedefs included in an attachment must be specified as a list of strings or a single string.' + self.idebug(target))
                        raise DAError('The defs in an attachment must be strings.' + self.idebug(target))
                        raise DAError('Referred to a non-existent def "' + def_key + '."  All defs must be defined before they are used.' + self.idebug(target))
                    raise DAError('A variable name cannot be None.' + self.idebug(target))
                    raise DAError('Unknown data type ' + str(type(target['metadata'])) + ' in attachment metadata.' + self.idebug(target))
                                raise DAError('Unknown data type ' + str(type(sub_data)) + ' in list in attachment metadata' + self.idebug(target))
                        raise DAError('Unknown data type ' + str(type(data)) + ' in key in attachment metadata' + self.idebug(target))
                        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 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 DAError('Unable to read content file ' + str(content_file) + ' after trying to find it at ' + str(file_to_read) + self.idebug(target))
                        raise DAError('Unknown data type in attachment "update references".' + self.idebug(target))
                    raise DAError('Fields supplied to attachment but no pdf template file or docx template file supplied' + self.idebug(target))
                    raise DAError('You cannot use a pdf template file and a docx template file at the same time' + self.idebug(target))
                            raise DAError('Unknown data type in attachment valid formats.' + self.idebug(target))
                            raise DAError('Valid formats cannot include "rtf to docx" when "docx template file" is used' + self.idebug(target))
                        raise DAError(template_type + ' template file supplied to attachment must be a string, dict, or list' + self.idebug(target))
                        raise DAError(template_type + ' template file supplied to attachment must be a string or dict' + self.idebug(target))
                    raise DAError('fields supplied to attachment must be a list or dictionary' + self.idebug(target))
                            raise DAError('docx template file supplied to attachment must be a string or dict' + self.idebug(target))
                                raise DAError("Missing docx template file " + template_file.original_reference())
                            raise DAError('The field variables must be expressed in the form of a list' + self.idebug(target))
                                raise DAError('The variable ' + str(varname) + " cannot be used in a code list" + self.idebug(target))
                            raise DAError('The raw field variables must be expressed in the form of a list' + self.idebug(target))
                                raise DAError('The variable ' + str(varname) + " cannot be used in a code list" + self.idebug(target))
                                raise DAError('The field code must be expressed in the form of a dictionary' + self.idebug(target))
                    raise DAError('Unknown data type in attachment valid formats.' + self.idebug(target))
                    raise DAError('Valid formats cannot include both "rtf to docx" and "docx."' + self.idebug(target))
                    raise DAError('Unknown data type in attachment persistent.' + self.idebug(target))
                    raise DAError('Unknown data type in attachment public.' + self.idebug(target))
                            raise DAError('Unknown data type in attachment allow privileges.' + self.idebug(target))
                            raise DAError('Unknown data type in attachment allow users.' + self.idebug(target))
                    raise DAError('Unknown data type in attachment hyperlink style.' + self.idebug(target))
                    raise DAError('Unknown data type in attachment pdf/a.' + self.idebug(target))
                    raise DAError('Unknown data type in attachment tagged pdf.' + self.idebug(target))
                raise DAError("No content provided in attachment." + self.idebug(target))
                    new_iterators[iterator_index] = str(list_indexno)
                                    raise DAError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason'])
                            raise DAError("Failure while processing field with datatype of object: " + err.__class__.__name__ + " " + str(err))
                        raise DAError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason'])
            raise DAError("Multiple choices need to be provided in list form.  " + self.idebug(the_list))
                raise DAError("Unknown data type for the_dict in parse_fields.  " + self.idebug(the_list))
                    raise DAError("Unknown data type in parse_fields:" + str(type(value)) + ".  " + self.idebug(the_list))
                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'):
                                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'])
                            for key in ('images', 'data_strings', 'convert_to_pdf_a', 'convert_to_tagged_pdf', 'password', 'owner_password', 'template_password', 'update_references', 'permissions', 'rendering_font'):
                                    raise DAError("prepare_attachment: error downloading " + str(the_filename) + ": " + str(err))
                        raise DAError("prepare_attachment: error obtaining template file from code: " + repr(the_orig_filename))
                                        raise DAError("Missing docx template file " + docx_reference.original_reference())
            raise DAError("Unknown data type in manual choices selection: " + re.sub(r'[<>]', '', repr(data)))
                        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 = 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__))
                        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 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 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 DAError("There appears to be a circularity.  Variables involved: " + ", ".join(variables_sought) + ".")
                                    raise DAError("Invalid name " + interview_status.current_info['arguments'][the_key])
                                    raise DAError("Invalid name " + interview_status.current_info['arguments'][the_key])
                                        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 DAError("NameError: " + str(the_exception) + extra)
                    raise DAError('Got error ' + str(the_error) + " " + traceback.format_exc() + "\nHistory was " + pprint.pformat(interview_status.seeking))
                        raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
                    raise DAError("no question available: " + str(qError))
                        raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
                    raise DAError("no question available: " + str(qError))
            raise DAError("There appears to be an infinite loop.  Variables in stack are " + ", ".join(sorted(variable_stack)) + ".")
        #     raise DAError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
                raise DAError("Infinite loop detected while looking for " + missing_var)
                                            raise DAError("askfor: error downloading " + str(the_filename) + ": " + str(err))
                                raise DAError("askfor: error obtaining template file from code: " + repr(the_orig_filename))
                    raise DAError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
                            raise DAError("NameError: " + str(the_exception) + extra)
                #    raise DAError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
                    # raise DAError("Problem setting that variable")
                    raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
                raise DAError("no question available in askfor: " + str(qError))
                    raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
                raise DAError("no question available in askfor: " + str(qError))
        raise DAError("Unknown data type in choices selection: " + re.sub(r'[<>]', '', repr(data)))
        raise DAError("Variable name " + saveas + " is invalid: " + parse_result['reason'])
        raise DAError("Missing docx template file " + os.path.basename(the_path))
        raise DAError("There was an error parsing the docx file: " + the_err.__class__.__name__ + " " + str(the_err))
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):
    if pdfa or not editable:
            subprocess_arguments.append('flatten')
        pdf.Root.AcroForm.NeedAppearances = True
        validators.DataRequired(word('Package name is required')), validate_name])
        validators.DataRequired(word('Package name is required')), validate_name])
    giturl = StringField(word('GitHub URL'))
    gitbranch = SelectField(word('GitHub Branch'))
    pippackage = StringField(word('Package on PyPI'))
    playground_name = StringField(word('Name'), [validators.Length(min=1, max=255)])
    file_name = StringField(word('Name'), [validators.Length(min=1, max=255)])
        validators.DataRequired(word('Project name is required')), validate_project_name])
        validators.DataRequired(word('Project name is required')), validate_project_name])
    github_url = StringField(word('GitHub URL'))
    github_branch = SelectField(word('GitHub Branch'))
    pypi = StringField(word('PyPI package'))
    original_file_name = StringField(word('Original Name'))
                                                              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="")
    github_branch_new = StringField(word('Name of new branch'))
    name = StringField(word('Name'), validators=[validators.Length(min=1, max=255)])
from docassemble.base.error import DAError, DAErrorNoEndpoint, DAErrorMissingVariable, DAErrorCompileError, DAValidationError, DAException, DANotFoundError, DAInvalidFilename

    do_redirect = False
            do_redirect = True
        if do_redirect:
                        for item in key_to_orig_key.keys():
            if ($(this).hasClass('combobox')){
            if ($(this).hasClass('combobox')){
    if isinstance(the_error, (DAError, DANotFoundError, DAInvalidFilename)):

    show_debug = not bool((not DEBUG) and isinstance(the_error, (DAError, DAInvalidFilename)))
from flask import flash, current_app, request, abort
from flask_login import current_user
from sqlalchemy import select
    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])
        da_registration_restrict_validator])
    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)])
    email = StringField(word('E-mail'))
    email = StringField(word('E-mail'), validators=[Optional(), Email(word('Must be a valid e-mail address'))])
    reason = StringField(word('Reason for needing developer account (optional)'))
    first_name = StringField(word('First name'), [validators.Length(min=0, max=255)])
    last_name = StringField(word('Last name'), [validators.Length(min=0, max=255)])
    phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255)])
    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)])
        validators.Email(word('Invalid e-mail address')),
        ])
Fixed code: ## [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.

&& 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"
"Field cannot contain HTML": Null
"There was an error. Please contact the system administrator.": Null
class DASourceError(DAError):
    pass

import convertapi
import requests
from pikepdf import Pdf

            need_appearances = False
            try:
                if original.Root.AcroForm.NeedAppearances:
                    need_appearances = True
            except:
                pass
                    if need_appearances is False:
                        try:
                            if additional_pdf.Root.AcroForm.NeedAppearances:
                                need_appearances = True
                        except:
                            pass
            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")
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
            raise DASourceError("A block must be in the form of a dictionary." + self.idebug(orig_data))
            raise DASourceError(orig_data['__error__'])
            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))
            raise DASourceError("This block is missing a 'question' directive." + self.idebug(data))
                raise DASourceError("A features section must be a dictionary." + self.idebug(data))
                    raise DASourceError("Table width in features must be an integer." + self.idebug(data))
                    raise DASourceError("progress bar multiplier in features must be between 0 and 1." + self.idebug(data))
            if 'pdftk' in data['features'] and data['features']['pdftk'] in (True, False):
                self.interview.options['use pdftk'] = data['features']['pdftk']
                    raise DASourceError("A features section checkin interval entry must be an integer." + self.idebug(data))
                    raise DASourceError("A features section checkin interval entry must be at least 1000, if not 0." + self.idebug(data))
                        raise DASourceError("A features section " + key + " entry must be a list or plain text." + self.idebug(data))
                        raise DASourceError("A features section " + key + " entry must be plain text." + self.idebug(data))
                        raise DASourceError("The " + key + " in features did not contain a valid date." + self.idebug(data))
                raise DASourceError("An only sets phrase must be text or a list." + 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))
                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))
                raise DASourceError("A 'translations' block must be a list" + self.idebug(data))
                    raise DASourceError("A 'translations' block must be a list of text items" + 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))
                    raise DASourceError("Invalid translations entry: " + item + ".  A translations entry must refer to a data sources file" + self.idebug(data))
                raise DASourceError("An on change block must be a dictionary." + self.idebug(data))
                raise DASourceError("An on change block must not contain any other keys." + 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))
                raise DASourceError("A sections list must be a list." + self.idebug(data))
                raise DASourceError("A progressive directive can only be used with sections." + self.idebug(data))
                raise DASourceError("A progressive directive can only be true or false." + self.idebug(data))
                raise DASourceError("An auto open directive can only be used with sections." + self.idebug(data))
                raise DASourceError("An auto open directive can only be true or false." + 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))
                raise DASourceError("Invalid machine learning storage entry: " + str(data['machine learning storage']) + self.idebug(data))
                        raise DASourceError("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 not a list, it must be plain text." + self.idebug(data))
                    raise DASourceError("A usedefs section must consist of a list of strings or a single string." + self.idebug(data))
                    raise DASourceError('Referred to a non-existent def "' + usedef + '."  All defs must be defined before they are used.' + self.idebug(data))
                raise DASourceError("You can only set the section from a question." + 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))
                raise DASourceError("You cannot set a resume button label 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))
                raise DASourceError("You cannot set list collect without a fields specifier." + self.idebug(data))
                raise DASourceError("Invalid data under list collect." + self.idebug(data))
                raise DASourceError("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 on this type of block." + self.idebug(data))
                    raise DASourceError("An attachment option must a dictionary." + self.idebug(data))
                                raise DASourceError('An initial yaml file must be a string.' + self.idebug(data))
                                raise DASourceError('An additional yaml file must be a string.' + self.idebug(data))
                            raise DASourceError('The template file must be a string.' + self.idebug(data))
                            raise DASourceError('The rtf template file must be a string.' + self.idebug(data))
                            raise DASourceError('The docx reference file must be a string.' + self.idebug(data))
                raise DASourceError("A script section must be plain text." + self.idebug(data))
                raise DASourceError("A css section must be plain text." + self.idebug(data))
            raise DASourceError("Only a code block can be marked as initial." + self.idebug(data))
                    raise DASourceError("An objects section cannot contain a nested list." + self.idebug(data))
                raise DASourceError("A data 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))
                # raise DASourceError("An objects section must be organized as a list." + self.idebug(data))
                    raise DASourceError("An objects section cannot contain a nested list." + 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))
                raise DASourceError("A 'ga id' must refer to text." + self.idebug(data))
                raise DASourceError("A 'segment id' must refer to text." + self.idebug(data))
                raise DASourceError("A 'segment' must refer to a dictionary." + self.idebug(data))
                    raise DASourceError("An 'id' under 'segment' must refer to text." + self.idebug(data))
                    raise DASourceError("An 'arguments' under 'segment' must refer to a dictionary." + self.idebug(data))
                        raise DASourceError("Each item under 'arguments' in a 'segment' must be plain text." + self.idebug(data))
                raise DASourceError("An 'order' block cannot be combined with another type of block." + self.idebug(data))
                raise DASourceError("An 'order' block must be a list." + self.idebug(data))
                raise DASourceError("The '" + key + "' section needs to be a dictionary, not a list or text." + 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))
                        raise DASourceError("An attribution in an 'image set' section cannot 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))
                raise DASourceError("A def name must be a string." + self.idebug(data))
                    raise DASourceError("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." + self.idebug(data))
                raise DASourceError("An interview help section must not be in the form of a list." + 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))
                        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))
                    raise DASourceError("A heading within an interview help section 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))
                raise DASourceError("No content section was found in an interview help section." + self.idebug(data))
                    raise DASourceError("Help label must be text, not a list or a dictionary." + self.idebug(data))
                raise DASourceError("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 a dictionary of text keys and text values." + self.idebug(data))
                raise DASourceError("A default validation messages block must be in the form of a dictionary." + self.idebug(data))
                    raise DASourceError("A validation messages block must be a dictionary of text keys and text values." + self.idebug(data))
                    raise DASourceError("A metadata directive cannot be mixed with other directives." + self.idebug(data))
                raise DASourceError("A metadata section must be organized as a dictionary." + self.idebug(data))
                raise DASourceError("A modules section must be organized as a list." + self.idebug(data))
                raise DASourceError("A reset section must be organized as a list." + self.idebug(data))
                raise DASourceError("An imports section must be organized as a list." + self.idebug(data))
                raise DASourceError("Terms must be organized as a dictionary or a list." + self.idebug(data))
                    raise DASourceError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data))
                raise DASourceError("Terms must be organized as a dictionary or a list." + self.idebug(data))
                    raise DASourceError("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))
                raise DASourceError("A terms section must be organized as a dictionary or a list." + self.idebug(data))
                        raise DASourceError("An auto terms section organized as a list must be a list of dictionary items." + self.idebug(data))
                raise DASourceError("An auto terms section must be organized as a dictionary or a list." + self.idebug(data))
                raise DASourceError("A default role must be a list or a string." + self.idebug(data))
                raise DASourceError("The role of a question must be a string or a list." + self.idebug(data))
                        raise DASourceError('An include section could not find the file ' + str(questionPath) + '.' + self.idebug(data))
                raise DASourceError("An include section must be organized as a list." + self.idebug(data))
                    raise DASourceError("An action buttons specifier must be a list." + self.idebug(data))
                        raise DASourceError("An action buttons item must be a dictionary." + self.idebug(data))
                        raise DASourceError("An action buttons item must contain an action in 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))
                        raise DASourceError("The arguments specifier in an action buttons item must refer to a dictionary." + self.idebug(data))
                        raise DASourceError("An action buttons item must contain a label in plain text." + self.idebug(data))
                        raise DASourceError("The color 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))
                        raise DASourceError("The placement 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))
                        raise DASourceError("The forget prior specifier in an action buttons item must refer to true or false." + self.idebug(data))
                            raise DASourceError("The arguments specifier in an action buttons item must refer to plain items." + self.idebug(data))
                raise DASourceError("An if statement must either be text or a list." + self.idebug(data))
                raise DASourceError("A validation code statement must be text." + self.idebug(data))
                        raise DASourceError("The orelse part of a require section must be organized as a dictionary." + self.idebug(data))
                    raise DASourceError("A require section must have an orelse part." + self.idebug(data))
                raise DASourceError("A require section must be organized as a list." + self.idebug(data))
        #         raise DASourceError("A role section must be text or a list." + self.idebug(data))
                raise DASourceError("A css class can only accompany a question." + self.idebug(data))
                raise DASourceError("A table css class can only accompany a question." + 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))
                                raise DASourceError("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 content declaration in a help block can only contain text." + self.idebug(data))
                    raise DASourceError("An audio 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))
            raise DASourceError("The required modifier can only be used on a signature block" + self.idebug(data))
                raise DASourceError("A check in event must be text or a list." + self.idebug(data))
                raise DASourceError("A yesno must refer to text." + self.idebug(data))
                raise DASourceError("A noyes must refer to text." + self.idebug(data))
                raise DASourceError("A yesnomaybe must refer to text." + self.idebug(data))
                raise DASourceError("A noyesmaybe must refer to text." + self.idebug(data))
                raise DASourceError("A sets phrase must be text or a list." + self.idebug(data))
                raise DASourceError("An event phrase must be text or a list." + self.idebug(data))
                    raise DASourceError("Missing or invalid variable name " + repr(data['field']) + "." + self.idebug(data))
                        raise DASourceError("Missing or invalid variable name " + repr(data['continue button field']) + "." + self.idebug(data))
                    raise DASourceError("A validation messages indicator must be a dictionary." + self.idebug(data))
                        raise DASourceError("A validation messages indicator must be a dictionary of text keys and text values." + self.idebug(data))
                raise DASourceError("A continue button field must be plain text." + self.idebug(data))
                raise DASourceError("A need phrase must be text or a list." + self.idebug(data))
                        raise DASourceError("If 'need' contains a dictionary it can only include keys 'pre' or 'post'." + self.idebug(data))
                            raise DASourceError("A need post 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))
                    raise DASourceError("In 'need', the items must be text strings." + self.idebug(data))
                raise DASourceError("The target of a template must be plain text." + self.idebug(data))
                raise DASourceError("A target directive can only be used with a template." + self.idebug(data))
                raise DASourceError("A table definition must have definitions for table, row, and column." + self.idebug(data))
                raise DASourceError("The row part of a table definition must be plain Python code." + self.idebug(data))
                raise DASourceError("The column part of a table definition must be a list." + self.idebug(data))
                    raise DASourceError("The column items in a table definition must be dictionaries." + self.idebug(data))
                    raise DASourceError("A column item in a table definition cannot be empty." + self.idebug(data))
                        raise DASourceError("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 attribute names" + self.idebug(data))
                    raise DASourceError("The edit directive must be a list of attributes, or True or False" + self.idebug(data))
                        raise DASourceError("The read only directive must be plain text referring to an attribute" + self.idebug(data))
                        raise DASourceError("The edit header directive must be text" + self.idebug(data))
                        raise DASourceError("The read only directive must be plain text referring to an attribute" + self.idebug(data))
                        raise DASourceError("The edit header directive must be text" + 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))
                        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))
                    #    raise DASourceError('The content file ' + str(data['content file']) + ' is not a markdown file ' + 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))
                raise DASourceError("A template must designate a single variable expressed as text." + self.idebug(data))
                raise DASourceError("The content of a template must be expressed as text." + self.idebug(data))
                raise DASourceError("A code section must be text, not a list or a dictionary." + self.idebug(data))
            #    raise DASourceError("A reconsider directive must be true or false." + self.idebug(data))
                    raise DASourceError("A reconsider directive must be true, false, a single variable or a list." + self.idebug(data))
                        raise DASourceError("A reconsider directive must refer to variable names expressed as text." + self.idebug(data))
                raise DASourceError("A undefine directive must a single variable or a list." + self.idebug(data))
                    raise DASourceError("A undefine directive must refer to variable names expressed as text." + self.idebug(data))
                raise DASourceError("A continue button field must be plain text." + self.idebug(data))
                raise DASourceError("The fields must be written in the form of a list." + 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))
                        raise DASourceError("Invalid datatype of hidden field." + self.idebug(data))
                        raise DASourceError("A multiple choice field must refer to a list of choices." + self.idebug(data))
                            raise DASourceError("An ajax field must have an associated action." + self.idebug(data))
                            raise DASourceError("An ajax field cannot contain a list of choices except through an action." + self.idebug(data))
                        raise DASourceError("A multiple choice field must refer to a list of choices." + self.idebug(data))
                    raise DASourceError("An object labeler can only be used with an object data type." + self.idebug(data))
                    raise DASourceError("You cannot combine note, html, and/or raw html in a single field." + self.idebug(data))
                            raise DASourceError("A validation messages indicator must be a dictionary." + self.idebug(data))
                                raise DASourceError("A validation messages indicator must be a dictionary of text keys and text values." + self.idebug(data))
                                    raise DASourceError("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))
                                    raise DASourceError("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))
                            raise DASourceError("A js show if or js hide 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))
                                        raise DASourceError(key + " cannot be combined with " + other_key + "." + self.idebug(data))
                                    raise DASourceError("You cannot mix js show 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))
                                raise DASourceError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data))
                            raise DASourceError("The keys of '" + key + "' cannot be a list" + self.idebug(data))
                            raise DASourceError("Invalid variable name in show if/hide if." + self.idebug(data))
                                        raise DASourceError(key + " cannot be combined with " + other_key + "." + self.idebug(data))
                                    raise DASourceError("You cannot mix js show 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))
                                raise DASourceError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data))
                            raise DASourceError("The keys of '" + key + "' cannot be a list" + self.idebug(data))
                            raise DASourceError("Invalid variable name in " + key + '.' + self.idebug(data))
                            raise DASourceError("A 'disable others' directive cannot be used with this data type." + self.idebug(data))
                            raise DASourceError("A 'disable 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))
                            raise DASourceError("A 'check others' directive must be True, False, or a list of variable names." + self.idebug(data))
                            raise DASourceError("If the datatype of a field is 'range', you must provide a min and a max." + self.idebug(data))
                                raise DASourceError("An exclude entry cannot be a dictionary." + self.idebug(data))
                            raise DASourceError("address autocomplete must be a Python expression, a dictionary, or a boolean value." + self.idebug(data))
                            raise DASourceError(key + " is not in the correct format." + self.idebug(data))
                                raise DASourceError(key + " has an invalid key " + repr(item) + "." + self.idebug(data))
                            raise DASourceError(key + ' must specify a width.' + self.idebug(data))
                                    raise DASourceError(key + " " + subkey + " must be a number between 1 and 12, or a Python expression." + self.idebug(data))
                                    raise DASourceError(key + " " + subkey + " must be True or False, or a Python expression." + self.idebug(data))
                            raise DASourceError(key + " is not in the correct format." + self.idebug(data))
                                raise DASourceError(key + " has an invalid key " + repr(item) + "." + self.idebug(data))
                            raise DASourceError(key + ' must specify a width.' + self.idebug(data))
                            raise DASourceError(key + " width must be a number between 1 and 12, or a Python expression." + self.idebug(data))
                            raise DASourceError("An action must be plain text" + self.idebug(data))
                            raise DASourceError("A trigger at must an integer greater than one" + self.idebug(data))
                                raise DASourceError("choices is not in appropriate format" + self.idebug(data))
                                raise DASourceError("An exclude entry cannot be a dictionary." + 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))
                            raise DASourceError("Fields in a 'field' section must be plain text." + self.idebug(data))
                            raise DASourceError("Missing or invalid variable name " + repr(field[key]) + "." + 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))
                            raise DASourceError("Syntax error: field label '" + str(key) + "' overwrites previous label, '" + str(field_info['label'].original_text) + "'" + self.idebug(data))
                            raise DASourceError("Fields in a 'field' section must be plain text." + self.idebug(data))
                            raise DASourceError("Missing or invalid variable name " + repr(field[key]) + " for key " + repr(key) + "." + 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))
                            raise DASourceError("choices exclude list is not in appropriate format" + self.idebug(data))
                            raise DASourceError("default list is not in appropriate format" + self.idebug(data))
                        raise DASourceError("Invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data))
                                    raise DASourceError("Missing or invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data))
                elif 'note' in field or 'html' in field or 'raw html' in field:
                    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))
                raise DASourceError("A review block without a continue button field cannot be mandatory." + self.idebug(data))
                raise DASourceError("The review must be written in the form of a list." + 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))
                            raise DASourceError("In a review block, you cannot mix help text with a button item." + self.idebug(data))  # , css, or script
                            #    raise DASourceError("Invalid variable name in fields." + self.idebug(data))
                            #    raise DASourceError("Missing or invalid variable name " + repr(the_saveas) + " ." + 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))
                                        raise DASourceError("The set statement must refer to a list." + self.idebug(data))
                                            raise DASourceError("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 with keys as variable names." + self.idebug(data))
                                                raise DASourceError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data))
                                        raise DASourceError("The follow up statement must refer to a list." + self.idebug(data))
                                            raise DASourceError("Invalid variable name in follow up " + repr(var) + "." + self.idebug(data))
                                            raise DASourceError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data))
                                        raise DASourceError("The " + command + " statement must refer to a list." + self.idebug(data))
                                            raise DASourceError("Invalid variable name " + repr(undef_var) + " in " + command + "." + self.idebug(data))
                                            raise DASourceError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data))
                                    raise DASourceError("An arguments directive must refer to a dictionary.  " + repr(data))
                                raise DASourceError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data))
                                raise DASourceError("Missing or invalid variable name " + repr(the_saveas) + " ." + 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))
                                        raise DASourceError("The set statement must refer to a list." + self.idebug(data))
                                            raise DASourceError("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 with keys as variable names." + self.idebug(data))
                                                raise DASourceError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data))
                                        raise DASourceError("The follow up statement must refer to a list." + self.idebug(data))
                                            raise DASourceError("Invalid variable name in follow up " + command + "." + self.idebug(data))
                                            raise DASourceError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data))
                                        raise DASourceError("The " + command + " statement must refer to a list." + self.idebug(data))
                                            raise DASourceError("Invalid variable name " + repr(undef_var) + " in fields " + command + "." + self.idebug(data))
                                            raise DASourceError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data))
                                    raise DASourceError("An arguments directive must refer to a dictionary.  " + repr(data))
                                raise DASourceError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data))
                                raise DASourceError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data))
                    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))
                raise DASourceError("No question type could be determined for this section." + self.idebug(data))
                #     raise DASourceError("Question ID " + str(self.id) + " results in duplicate question name")
                    raise DASourceError('Unknown data type in attachment skip undefined.' + self.idebug(target))
                    raise DASourceError("A checkbox export value must be a string." + self.idebug(target))
                    raise DASourceError("A decimal places directive must be an integer or string." + self.idebug(target))
                        raise DASourceError('An initial yaml file must be a string.' + self.idebug(target))
                        raise DASourceError('An additional yaml file must be a string.' + self.idebug(target))
                    raise DASourceError('The template file must be a string.' + self.idebug(target))
                    raise DASourceError('The rtf template file must be a string.' + self.idebug(target))
                    raise DASourceError('The docx reference file must be a 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))
                        raise DASourceError('The defs in an attachment must be strings.' + 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))
                    raise DASourceError('A variable name cannot be None.' + self.idebug(target))
                    raise DASourceError('Unknown data type ' + str(type(target['metadata'])) + ' in attachment metadata.' + self.idebug(target))
                                raise DASourceError('Unknown data type ' + str(type(sub_data)) + ' in list in attachment metadata' + self.idebug(target))
                        raise DASourceError('Unknown data type ' + str(type(data)) + ' in key in attachment metadata' + 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))
                            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))
                            raise DASourceError('Unable to read content file ' + str(content_file) + ' after trying to find it at ' + str(file_to_read) + self.idebug(target))
                        raise DASourceError('Unknown data type in attachment "update references".' + self.idebug(target))
                    raise DASourceError('Fields supplied to attachment but no pdf template file or docx template file supplied' + self.idebug(target))
                    raise DASourceError('You cannot use a pdf template file and a docx template file at the same time' + self.idebug(target))
                            raise DASourceError('Unknown data type in attachment valid formats.' + self.idebug(target))
                            raise DASourceError('Valid formats cannot include "rtf to docx" when "docx template file" is used' + self.idebug(target))
                        raise DASourceError(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 or dict' + self.idebug(target))
                    raise DASourceError('fields supplied to attachment must be a list or dictionary' + self.idebug(target))
                            raise DASourceError('docx template file supplied to attachment must be a string or dict' + self.idebug(target))
                                raise DASourceError("Missing docx template file " + template_file.original_reference())
                            raise DASourceError('The field variables must be expressed in the form of a list' + self.idebug(target))
                                raise DASourceError('The variable ' + str(varname) + " cannot be used in a code list" + self.idebug(target))
                            raise DASourceError('The raw field variables must be expressed in the form of a list' + self.idebug(target))
                                raise DASourceError('The variable ' + str(varname) + " cannot be used in a code list" + self.idebug(target))
                                raise DASourceError('The field code must be expressed in the form of a dictionary' + self.idebug(target))
                    raise DASourceError('Unknown data type in attachment valid formats.' + self.idebug(target))
                    raise DASourceError('Valid formats cannot include both "rtf to docx" and "docx."' + self.idebug(target))
                    raise DASourceError('Unknown data type in attachment persistent.' + self.idebug(target))
                    raise DASourceError('Unknown data type in attachment public.' + self.idebug(target))
                            raise DASourceError('Unknown data type in attachment allow privileges.' + self.idebug(target))
                            raise DASourceError('Unknown data type in attachment allow users.' + self.idebug(target))
                    raise DASourceError('Unknown data type in attachment hyperlink style.' + 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))
                    raise DASourceError('Unknown data type in attachment tagged pdf.' + self.idebug(target))
                raise DASourceError("No content provided in attachment." + self.idebug(target))
                    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'])
                                    raise DASourceError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason'])
                            raise DASourceError("Failure while processing field with datatype of object: " + err.__class__.__name__ + " " + str(err))
                        raise DASourceError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason'])
            raise DASourceError("Multiple choices need to be provided in list form.  " + self.idebug(the_list))
                raise DASourceError("Unknown data type for the_dict in parse_fields.  " + self.idebug(the_list))
                    raise DASourceError("Unknown data type in parse_fields:" + str(type(value)) + ".  " + self.idebug(the_list))
                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'):
                                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'])
                            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'):
                                    raise DASourceError("prepare_attachment: error downloading " + str(the_filename) + ": " + str(err))
                        raise DASourceError("prepare_attachment: error obtaining template file from code: " + repr(the_orig_filename))
            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)
                                        raise DASourceError("Missing docx template file " + docx_reference.original_reference())
            raise DASourceError("Unknown data type in manual choices selection: " + re.sub(r'[<>]', '', repr(data)))
                        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)}')
                        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 DASourceError(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"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"Syntax Error: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}")
                    raise DASourceError("There appears to be a circularity.  Variables involved: " + ", ".join(variables_sought) + ".")
                                    raise DASourceError("Invalid name " + interview_status.current_info['arguments'][the_key])
                                    raise DASourceError("Invalid name " + interview_status.current_info['arguments'][the_key])
                                        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'))
                                raise DASourceError("NameError: " + str(the_exception) + extra)
                    raise DASourceError('Got error ' + str(the_error) + " " + traceback.format_exc() + "\nHistory was " + pprint.pformat(interview_status.seeking))
                        raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
                    raise DASourceError("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))
            raise DASourceError("There appears to be an infinite loop.  Variables in stack are " + ", ".join(sorted(variable_stack)) + ".")
        #     raise DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
                raise DASourceError("Infinite loop detected while looking for " + missing_var)
                                            raise DASourceError("askfor: error downloading " + str(the_filename) + ": " + str(err))
                                raise DASourceError("askfor: error obtaining template file from code: " + repr(the_orig_filename))
                    raise DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
                            raise DASourceError("NameError: " + str(the_exception) + extra)
                #    raise DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
                    # raise DASourceError("Problem setting that variable")
                    raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
                raise DASourceError("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))
        raise DASourceError("Unknown data type in choices selection: " + re.sub(r'[<>]', '', repr(data)))
        raise DASourceError("Variable name " + saveas + " is invalid: " + parse_result['reason'])
        raise DASourceError("Missing docx template file " + os.path.basename(the_path))
        raise DASourceError("There was an error parsing the docx file: " + the_err.__class__.__name__ + " " + str(the_err))
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 pdfa or not editable or use_pdftk:
            if pdfa or not editable:
                subprocess_arguments.append('flatten')
            else:
                subprocess_arguments.append('need_appearances')
        pdf.Root.AcroForm.NeedAppearances = True
        pdf.generate_appearance_streams()
        pdf.Root.AcroForm.NeedAppearances = True
from docassemble.webapp.validators import html_validator
        validators.DataRequired(word('Package name is required')), validate_name, html_validator])
        validators.DataRequired(word('Package name is required')), validate_name, html_validator])
    giturl = StringField(word('GitHub URL'), validators=[html_validator])
    gitbranch = SelectField(word('GitHub Branch'), validators=[html_validator])
    pippackage = StringField(word('Package on PyPI'), validators=[html_validator])
    playground_name = StringField(word('Name'), [validators.Length(min=1, max=255), html_validator])
    file_name = StringField(word('Name'), [validators.Length(min=1, max=255), html_validator])
        validators.DataRequired(word('Project name is required')), validate_project_name, html_validator])
        validators.DataRequired(word('Project name is required')), validate_project_name, html_validator])
    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])
    original_file_name = StringField(word('Original Name'), validators=[html_validator])
                                                              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="")
    github_branch_new = StringField(word('Name of new branch'), validators=[html_validator])
    name = StringField(word('Name'), validators=[validators.Length(min=1, max=255), html_validator])
from docassemble.base.error import DAError, DAErrorNoEndpoint, DAErrorMissingVariable, DAErrorCompileError, DAValidationError, DAException, DANotFoundError, DAInvalidFilename, DASourceError
    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
    if len(safe_url) > 0 and safe_url[0] not in ('?', '#', '/'):
        safe_url = '/' + safe_url
    safe_url = re.sub(r'^//+', '/', safe_url)

    do_a_redirect = False
            do_a_redirect = True
        if do_a_redirect:
                        for item in key_to_orig_key:
            if ($(this).prop('tagName') == 'INPUT' && $(this).hasClass('combobox')){
            if ($(this).prop('tagName') == 'INPUT' && $(this).hasClass('combobox')){
    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)):
        try:
            logmessage(errmess)
        except:
            logmessage("Could not log the error message")
    show_debug = not bool((not (DEBUG and daconfig.get('development site is protected', False))) and isinstance(the_error, (DAError, DAInvalidFilename)))

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
    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])
        da_registration_restrict_validator,
        html_validator])
    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])
    email = StringField(word('E-mail'), validators=[Email(word('Must be a valid e-mail address')), html_validator])
    email = StringField(word('E-mail'), validators=[Optional(), Email(word('Must be a valid e-mail address')), html_validator])
    reason = StringField(word('Reason for needing developer account (optional)'), validators=[html_validator])
    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])
    phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255), html_validator])
    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])
    phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255), html_validator])
        validators.Email(word('Invalid e-mail address'))
    ])
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'))