From 4801ac7ff7c90df00ac09523077930cdb6dea2aa Mon Sep 17 00:00:00 2001 From: Jonathan Pyle Date: Thu, 29 Feb 2024 12:43:07 -0500 Subject: [PATCH] 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 --- CHANGELOG.md | 27 + Dockerfile | 8 +- .../base/data/sources/base-words.yml | 2 + docassemble_base/docassemble/base/error.py | 3 + docassemble_base/docassemble/base/pandoc.py | 24 +- docassemble_base/docassemble/base/parse.py | 744 +++++++++--------- docassemble_base/docassemble/base/pdftk.py | 13 +- .../docassemble/webapp/develop.py | 45 +- .../docassemble/webapp/server.py | 38 +- .../docassemble/webapp/users/forms.py | 72 +- .../docassemble/webapp/validators.py | 10 + 11 files changed, 548 insertions(+), 438 deletions(-) create mode 100644 docassemble_webapp/docassemble/webapp/validators.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d14b7b3f..1504b6c4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Change Log +## [1.4.97] - 2024-02-29 + +### Added +- The `pdftk` option under `attachment` and `features` for filling in + `pdf template file` attachments using pdftk instead of pikepdf. +### Changed +- During the Docker image build process, `pandoc` will run once, so + that the first user to assemble a document with `pandoc` will not + experience slowness due to LaTeX needing to generate files. +- Appearance streams will be generated when using `pdf template file`. +- Error messages related to problems in the source code will no longer + be displayed to the user unless the user is an administrator or + developer. If you want these error messages to appear to all users, + set `debug: True` and `development site is protected: True` in the + Configuration. The error messages will be available in + `docassemble.log`. +### Fixed +- Fixed security issue identified by Riyush Ghimire, affecting + versions 1.4.53 to 1.4.96, that could cause contents of files in the + filesystem to be revealed. This is a high severity issue and + upgrading as soon as possible is recommended. +- Fixed security issue identified by Riyush Ghimire, affecting + versions up to 1.4.96, that allowed an open redirect URL to be formed. +- Fixed security issue identified by Riyush Ghimire, affecting + versions up to 1.4.96, that would allow HTML or JavaScript + injection. + ## [1.4.96] - 2024-02-14 ### Fixed diff --git a/Dockerfile b/Dockerfile index 7167ed8af..c66bb1b1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -122,7 +122,13 @@ RUN bash -c \ && python /tmp/docassemble/Docker/nltkdownload.py \ && cd /var/www/nltk_data/corpora \ && unzip -o wordnet.zip \ -&& unzip -o omw-1.4.zip" +&& unzip -o omw-1.4.zip \ +&& cd /tmp \ +&& mkdir -p /tmp/conv \ +&& pandoc --pdf-engine=lualatex -M latextmpdir=./conv -M pdfa=false /usr/share/docassemble/local3.10/lib/python3.10/site-packages/docassemble/base/data/templates/Legal-Template.yml --template=/usr/share/docassemble/local3.10/lib/python3.10/site-packages/docassemble/base/data/templates/Legal-Template.tex --from=markdown+raw_tex-latex_macros -s -o /tmp/temp.pdf /usr/share/docassemble/local3.10/lib/python3.10/site-packages/docassemble/base/data/templates/hello.md \ +&& rm /tmp/temp.pdf \ +&& pandoc --pdf-engine=lualatex -M latextmpdir=./conv -M pdfa=false --template=/usr/share/docassemble/local3.10/lib/python3.10/site-packages/docassemble/base/data/templates/Legal-Template.rtf -s -o /tmp/temp.rtf /usr/share/docassemble/local3.10/lib/python3.10/site-packages/docassemble/base/data/templates/hello.md \ +&& rm /tmp/temp.rtf" USER root RUN rm -rf /tmp/docassemble 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 @@ -311,6 +311,7 @@ "Falkland Islands (Malvinas)": Null "false": Null "Faroe Islands": Null +"Field cannot contain HTML": Null "Fiji": Null "File could not be converted: ": Null "File deleted.": Null @@ -1031,6 +1032,7 @@ "There is already a username and password on this system with the e-mail address": Null "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.": Null "There was an error.": Null +"There was an error. Please contact the system administrator.": Null "There was an error updating the packages.": Null "There was an error with the synchronization.": Null "There was a problem connecting to GitHub. Please check your GitHub configuration and try again.": Null 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 @@ -27,6 +27,9 @@ def __str__(self): return str(self.value) +class DASourceError(DAError): + pass + class DANotFoundError(Exception): pass 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 @@ -10,6 +10,9 @@ import random import mimetypes import urllib.request +import convertapi +import requests +from pikepdf import Pdf import docassemble.base.filter import docassemble.base.functions from docassemble.base.config import daconfig @@ -17,9 +20,6 @@ from docassemble.base.pdfa import pdf_to_pdfa from docassemble.base.pdftk import pdf_encrypt from docassemble.base.error import DAError, DAException -import convertapi -import requests -from pikepdf import Pdf style_find = re.compile(r'{\s*(\\s([1-9])[^\}]+)\\sbasedon[^\}]+heading ([0-9])', flags=re.DOTALL) PANDOC_PATH = daconfig.get('pandoc', 'pandoc') @@ -802,13 +802,31 @@ def concatenate_files(path_list, pdfa=False, password=None, owner_password=None) new_path_list.append(path) if len(new_path_list) == 0: raise DAError("concatenate_files: no valid files to concatenate") + if len(new_path_list) == 1: shutil.copyfile(new_path_list[0], pdf_file.name) else: with Pdf.open(new_path_list[0]) as original: + need_appearances = False + try: + if original.Root.AcroForm.NeedAppearances: + need_appearances = True + except: + pass for additional_file in new_path_list[1:]: with Pdf.open(additional_file) as additional_pdf: + if need_appearances is False: + try: + if additional_pdf.Root.AcroForm.NeedAppearances: + need_appearances = True + except: + pass original.pages.extend(additional_pdf.pages) + if need_appearances: + try: + original.Root.AcroForm.NeedAppearances = True + except: + logmessage("concatenate_files: an additional file had an AcroForm with NeedAppearances but setting NeedAppearances on the final document resulted in an error") original.save(pdf_file.name) if pdfa: pdf_to_pdfa(pdf_file.name) 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 @@ -49,7 +49,7 @@ import docassemble.base.filter import docassemble.base.pdftk import docassemble.base.file_docx -from docassemble.base.error import DAError, DANotFoundError, MandatoryQuestion, DAErrorNoEndpoint, DAErrorMissingVariable, ForcedNameError, QuestionError, ResponseError, BackgroundResponseError, BackgroundResponseActionError, CommandError, CodeExecute, DAValidationError, ForcedReRun, LazyNameError, DAAttributeError, DAIndexError, DAException, DANameError +from docassemble.base.error import DAError, DANotFoundError, MandatoryQuestion, DAErrorNoEndpoint, DAErrorMissingVariable, ForcedNameError, QuestionError, ResponseError, BackgroundResponseError, BackgroundResponseActionError, CommandError, CodeExecute, DAValidationError, ForcedReRun, LazyNameError, DAAttributeError, DAIndexError, DAException, DANameError, DASourceError import docassemble.base.functions import docassemble.base.util from docassemble.base.functions import pickleable_objects, word, get_language, RawValue, get_config @@ -2063,9 +2063,9 @@ def id_debug(self, data): def __init__(self, orig_data, caller, **kwargs): if not isinstance(orig_data, dict): - raise DAError("A block must be in the form of a dictionary." + self.idebug(orig_data)) + raise DASourceError("A block must be in the form of a dictionary." + self.idebug(orig_data)) if '__error__' in orig_data: - raise DAError(orig_data['__error__']) + raise DASourceError(orig_data['__error__']) data = {} for key, value in orig_data.items(): data[key.lower()] = value @@ -2133,9 +2133,9 @@ def __init__(self, orig_data, caller, **kwargs): if directive in data: num_directives += 1 if num_directives > 1: - raise DAError("There can only be one directive in a question. You had more than one.\nThe directives are yesno, noyes, yesnomaybe, noyesmaybe, fields, buttons, choices, dropdown, combobox, signature, and review" + self.idebug(data)) + raise DASourceError("There can only be one directive in a question. You had more than one.\nThe directives are yesno, noyes, yesnomaybe, noyesmaybe, fields, buttons, choices, dropdown, combobox, signature, and review" + self.idebug(data)) if num_directives > 0 and 'question' not in data: - raise DAError("This block is missing a 'question' directive." + self.idebug(data)) + raise DASourceError("This block is missing a 'question' directive." + self.idebug(data)) if self.interview.debug: for key in data: if key not in ('features', 'scan for variables', 'only sets', 'question', 'code', 'event', 'translations', 'default language', 'on change', 'sections', 'progressive', 'auto open', 'section', 'machine learning storage', 'language', 'prevent going back', 'back button', 'usedefs', 'continue button label', 'continue button color', 'resume button label', 'resume button color', 'back button label', 'corner back button label', 'skip undefined', 'list collect', 'mandatory', 'attachment options', 'script', 'css', 'initial', 'default role', 'command', 'objects from file', 'use objects', 'data', 'variable name', 'data from code', 'objects', 'id', 'ga id', 'segment id', 'segment', 'supersedes', 'order', 'image sets', 'images', 'def', 'mako', 'interview help', 'default screen parts', 'default validation messages', 'generic object', 'generic list object', 'comment', 'metadata', 'modules', 'reset', 'imports', 'terms', 'auto terms', 'role', 'include', 'action buttons', 'if', 'validation code', 'require', 'orelse', 'attachment', 'attachments', 'attachment code', 'attachments code', 'allow emailing', 'allow downloading', 'email subject', 'email body', 'email template', 'email address default', 'progress', 'zip filename', 'action', 'backgroundresponse', 'response', 'binaryresponse', 'all_variables', 'response filename', 'content type', 'redirect url', 'null response', 'sleep', 'include_internal', 'css class', 'table css class', 'response code', 'subquestion', 'reload', 'help', 'audio', 'video', 'decoration', 'signature', 'under', 'pre', 'post', 'right', 'check in', 'yesno', 'noyes', 'yesnomaybe', 'noyesmaybe', 'sets', 'event', 'choices', 'buttons', 'dropdown', 'combobox', 'field', 'shuffle', 'review', 'need', 'depends on', 'target', 'table', 'rows', 'columns', 'require gathered', 'allow reordering', 'edit', 'delete buttons', 'confirm', 'read only', 'edit header', 'confirm', 'show if empty', 'template', 'content file', 'content', 'subject', 'reconsider', 'undefine', 'continue button field', 'fields', 'indent', 'url', 'default', 'datatype', 'extras', 'allowed to set', 'show incomplete', 'not available label', 'required', 'always include editable files', 'question metadata', 'include attachment notice', 'include download tab', 'manual attachment list', 'breadcrumb', 'tabular', 'hide continue button', 'disable continue button', 'pen color', 'gathered'): @@ -2143,12 +2143,12 @@ def __init__(self, orig_data, caller, **kwargs): if 'features' in data: should_append = False if not isinstance(data['features'], dict): - raise DAError("A features section must be a dictionary." + self.idebug(data)) + raise DASourceError("A features section must be a dictionary." + self.idebug(data)) if 'use catchall' in data['features'] and isinstance(data['features']['use catchall'], bool): self.interview.options['use catchall'] = data['features']['use catchall'] if 'table width' in data['features']: if not isinstance(data['features']['table width'], int): - raise DAError("Table width in features must be an integer." + self.idebug(data)) + raise DASourceError("Table width in features must be an integer." + self.idebug(data)) self.interview.table_width = data['features']['table width'] if 'progress bar' in data['features'] and isinstance(data['features']['progress bar'], bool): self.interview.use_progress_bar = data['features']['progress bar'] @@ -2160,7 +2160,7 @@ def __init__(self, orig_data, caller, **kwargs): self.interview.progress_bar_method = data['features']['progress bar method'] if 'progress bar multiplier' in data['features'] and isinstance(data['features']['progress bar multiplier'], (int, float)): if data['features']['progress bar multiplier'] <= 0.0 or data['features']['progress bar multiplier'] >= 1.0: - raise DAError("progress bar multiplier in features must be between 0 and 1." + self.idebug(data)) + raise DASourceError("progress bar multiplier in features must be between 0 and 1." + self.idebug(data)) self.interview.progress_bar_method = data['features']['progress bar multiplier'] if 'question back button' in data['features'] and isinstance(data['features']['question back button'], bool): self.interview.question_back_button = data['features']['question back button'] @@ -2195,6 +2195,8 @@ def __init__(self, orig_data, caller, **kwargs): self.interview.recursion_limit = data['features']['recursion limit'] if 'pdf/a' in data['features'] and data['features']['pdf/a'] in (True, False): self.interview.use_pdf_a = data['features']['pdf/a'] + if 'pdftk' in data['features'] and data['features']['pdftk'] in (True, False): + self.interview.options['use pdftk'] = data['features']['pdftk'] if 'tagged pdf' in data['features'] and data['features']['tagged pdf'] in (True, False): self.interview.use_tagged_pdf = data['features']['tagged pdf'] if 'bootstrap theme' in data['features'] and data['features']['bootstrap theme']: @@ -2231,9 +2233,9 @@ def __init__(self, orig_data, caller, **kwargs): self.interview.custom_data_types.add(item) if 'checkin interval' in data['features']: if not isinstance(data['features']['checkin interval'], int): - raise DAError("A features section checkin interval entry must be an integer." + self.idebug(data)) + raise DASourceError("A features section checkin interval entry must be an integer." + self.idebug(data)) if data['features']['checkin interval'] > 0 and data['features']['checkin interval'] < 1000: - raise DAError("A features section checkin interval entry must be at least 1000, if not 0." + self.idebug(data)) + raise DASourceError("A features section checkin interval entry must be at least 1000, if not 0." + self.idebug(data)) self.interview.options['checkin interval'] = data['features']['checkin interval'] if 'hide corner interface' in data['features']: self.interview.options['hide corner interface'] = data['features']['hide corner interface'] @@ -2242,7 +2244,7 @@ def __init__(self, orig_data, caller, **kwargs): if isinstance(data['features'][key], list): the_list = data['features'][key] elif isinstance(data['features'][key], dict): - raise DAError("A features section " + key + " entry must be a list or plain text." + self.idebug(data)) + raise DASourceError("A features section " + key + " entry must be a list or plain text." + self.idebug(data)) else: the_list = [data['features'][key]] for the_file in the_list: @@ -2252,11 +2254,11 @@ def __init__(self, orig_data, caller, **kwargs): for key in ('default date min', 'default date max'): if key in data['features']: if not isinstance(data['features'][key], str): - raise DAError("A features section " + key + " entry must be plain text." + self.idebug(data)) + raise DASourceError("A features section " + key + " entry must be plain text." + self.idebug(data)) try: self.interview.options[key] = dateutil.parser.parse(data['features'][key]).astimezone(zoneinfo.ZoneInfo(docassemble.base.functions.get_default_timezone())) except: - raise DAError("The " + key + " in features did not contain a valid date." + self.idebug(data)) + raise DASourceError("The " + key + " in features did not contain a valid date." + self.idebug(data)) if 'field' in data and not ('yesno' in data or 'noyes' in data or 'yesnomaybe' in data or 'noyesmaybe' in data or 'buttons' in data or 'choices' in data or 'dropdown' in data or 'combobox' in data): data['continue button field'] = data['field'] del data['field'] @@ -2272,23 +2274,23 @@ def __init__(self, orig_data, caller, **kwargs): for key in data['only sets']: self.fields_used.add(key) else: - raise DAError("An only sets phrase must be text or a list." + self.idebug(data)) + raise DASourceError("An only sets phrase must be text or a list." + self.idebug(data)) self.scan_for_variables = False if 'question' in data and 'code' in data: - raise DAError("A block can be a question block or a code block but cannot be both at the same time." + self.idebug(data)) + raise DASourceError("A block can be a question block or a code block but cannot be both at the same time." + self.idebug(data)) if 'event' in data: if 'field' in data or 'fields' in data or 'yesno' in data or 'noyes' in data: - raise DAError("The 'event' designator is for special screens that do not gather information and can only be used with 'buttons' or with no other controls." + self.idebug(data)) + raise DASourceError("The 'event' designator is for special screens that do not gather information and can only be used with 'buttons' or with no other controls." + self.idebug(data)) if 'translations' in data: should_append = False if not isinstance(data['translations'], list): - raise DAError("A 'translations' block must be a list" + self.idebug(data)) + raise DASourceError("A 'translations' block must be a list" + self.idebug(data)) tr_todo = [] for item in data['translations']: if not isinstance(item, str): - raise DAError("A 'translations' block must be a list of text items" + self.idebug(data)) + raise DASourceError("A 'translations' block must be a list of text items" + self.idebug(data)) if not (item.endswith('.xlsx') or item.endswith('.xlf') or item.endswith('.xliff')): - raise DAError("Invalid translations entry '" + item + "'. A translations entry must refer to a file ending in .xlsx, .xlf, or .xliff." + self.idebug(data)) + raise DASourceError("Invalid translations entry '" + item + "'. A translations entry must refer to a file ending in .xlsx, .xlf, or .xliff." + self.idebug(data)) parts = item.split(":") if len(parts) == 1: item = re.sub(r'^data/sources/', '', item) @@ -2299,7 +2301,7 @@ def __init__(self, orig_data, caller, **kwargs): elif len(parts) == 2 and parts[0].startswith('docassemble.') and parts[1].startswith('data/sources/'): tr_todo.append(item) else: - raise DAError("Invalid translations entry: " + item + ". A translations entry must refer to a data sources file" + self.idebug(data)) + raise DASourceError("Invalid translations entry: " + item + ". A translations entry must refer to a data sources file" + self.idebug(data)) for item in tr_todo: self.interview.translations.append(item) if item.endswith(".xlsx"): @@ -2387,12 +2389,12 @@ def __init__(self, orig_data, caller, **kwargs): 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, '', 'exec')) @@ -2400,26 +2402,26 @@ def __init__(self, orig_data, caller, **kwargs): if 'sections' in data: should_append = False if not isinstance(data['sections'], list): - raise DAError("A sections list must be a list." + self.idebug(data)) + raise DASourceError("A sections list must be a list." + self.idebug(data)) the_language = data.get('language', '*') self.interview.sections[the_language] = data['sections'] if 'progressive' in data: if 'sections' not in data: - raise DAError("A progressive directive can only be used with sections." + self.idebug(data)) + raise DASourceError("A progressive directive can only be used with sections." + self.idebug(data)) if not isinstance(data['progressive'], bool): - raise DAError("A progressive directive can only be true or false." + self.idebug(data)) + raise DASourceError("A progressive directive can only be true or false." + self.idebug(data)) self.interview.sections_progressive = data['progressive'] if 'auto open' in data: if 'sections' not in data: - raise DAError("An auto open directive can only be used with sections." + self.idebug(data)) + raise DASourceError("An auto open directive can only be used with sections." + self.idebug(data)) if not isinstance(data['auto open'], bool): - raise DAError("An auto open directive can only be true or false." + self.idebug(data)) + raise DASourceError("An auto open directive can only be true or false." + self.idebug(data)) self.interview.sections_auto_open = data['auto open'] if 'machine learning storage' in data: should_append = False new_storage = data['machine learning storage'] if not new_storage.endswith('.json'): - raise DAError("Invalid machine learning storage entry '" + str(data['machine learning storage']) + ".' A machine learning storage entry must refer to a file ending in .json." + self.idebug(data)) + raise DASourceError("Invalid machine learning storage entry '" + str(data['machine learning storage']) + ".' A machine learning storage entry must refer to a file ending in .json." + self.idebug(data)) parts = new_storage.split(":") if len(parts) == 1: new_storage = re.sub(r'^data/sources/', '', new_storage) @@ -2430,7 +2432,7 @@ def __init__(self, orig_data, caller, **kwargs): elif len(parts) == 2 and parts[0].startswith('docassemble.') and parts[1].startswith('data/sources/'): self.interview.set_ml_store(data['machine learning storage']) else: - raise DAError("Invalid machine learning storage entry: " + str(data['machine learning storage']) + self.idebug(data)) + raise DASourceError("Invalid machine learning storage entry: " + str(data['machine learning storage']) + self.idebug(data)) if 'language' in data: self.language = data['language'] else: @@ -2448,13 +2450,13 @@ def __init__(self, orig_data, caller, **kwargs): 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'], '', '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'], '', 'eval') self.find_fields_in(data['hide continue button']) @@ -2469,24 +2471,24 @@ def __init__(self, orig_data, caller, **kwargs): usedefs = [data['usedefs']] for usedef in usedefs: if isinstance(usedef, (dict, list, set, bool)): - raise DAError("A usedefs section must consist of a list of strings or a single string." + self.idebug(data)) + raise DASourceError("A usedefs section must consist of a list of strings or a single string." + self.idebug(data)) if usedef not in self.interview.defs: - raise DAError('Referred to a non-existent def "' + usedef + '." All defs must be defined before they are used.' + self.idebug(data)) + raise DASourceError('Referred to a non-existent def "' + usedef + '." All defs must be defined before they are used.' + self.idebug(data)) defs.extend(self.interview.defs[usedef]) definitions = "\n".join(defs) + "\n" else: definitions = "" if 'section' in data: if 'question' not in data: - raise DAError("You can only set the section from a question." + self.idebug(data)) + raise DASourceError("You can only set the section from a question." + self.idebug(data)) self.section = TextObject(definitions + str(data['section']), question=self) if 'continue button label' in data: if 'yesno' in data or 'noyes' in data or 'yesnomaybe' in data or 'noyesmaybe' in data or 'buttons' in data: - raise DAError("You cannot set a continue button label if the type of question is yesno, noyes, yesnomaybe, noyesmaybe, or buttons." + self.idebug(data)) + raise DASourceError("You cannot set a continue button label if the type of question is yesno, noyes, yesnomaybe, noyesmaybe, or buttons." + self.idebug(data)) self.continuelabel = TextObject(definitions + str(data['continue button label']), question=self) if 'resume button label' in data: if 'review' not in data: - raise DAError("You cannot set a resume button label if the type of question is not review." + self.idebug(data)) + raise DASourceError("You cannot set a resume button label if the type of question is not review." + self.idebug(data)) self.continuelabel = TextObject(definitions + str(data['resume button label']), question=self) if 'continue button color' in data: self.continuecolor = TextObject(definitions + str(data['continue button color']), question=self) @@ -2498,12 +2500,12 @@ def __init__(self, orig_data, caller, **kwargs): 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']), '', 'eval') elif isinstance(data['list collect'], dict): @@ -2522,14 +2524,14 @@ def __init__(self, orig_data, caller, **kwargs): if 'add another label' in data['list collect']: self.list_collect_add_another_label = TextObject(definitions + str(data['list collect']['add another label']), question=self) else: - raise DAError("Invalid data under list collect." + self.idebug(data)) + raise DASourceError("Invalid data under list collect." + self.idebug(data)) if 'mandatory' in data: if 'initial' in data: - raise DAError("You cannot use the mandatory modifier and the initial modifier at the same time." + self.idebug(data)) + raise DASourceError("You cannot use the mandatory modifier and the initial modifier at the same time." + self.idebug(data)) if 'id' not in data and self.interview.debug and self.interview.source.package.startswith('docassemble.playground'): self.interview.issue['mandatory_id'] = True if 'question' not in data and 'code' not in data and 'objects' not in data and 'attachment' not in data and 'data' not in data and 'data from code' not in data: - raise DAError("You cannot use the mandatory modifier on this type of block." + self.idebug(data)) + raise DASourceError("You cannot use the mandatory modifier on this type of block." + self.idebug(data)) if data['mandatory'] is True: self.is_mandatory = True self.mandatory_code = None @@ -2552,7 +2554,7 @@ def __init__(self, orig_data, caller, **kwargs): data['attachment options'] = [data['attachment options']] for attachment_option in data['attachment options']: if not isinstance(attachment_option, dict): - raise DAError("An attachment option must a dictionary." + self.idebug(data)) + raise DASourceError("An attachment option must a dictionary." + self.idebug(data)) for key in attachment_option: value = attachment_option[key] if key == 'initial yaml': @@ -2564,7 +2566,7 @@ def __init__(self, orig_data, caller, **kwargs): the_list = [value] for yaml_file in the_list: if not isinstance(yaml_file, str): - raise DAError('An initial yaml file must be a string.' + self.idebug(data)) + raise DASourceError('An initial yaml file must be a string.' + self.idebug(data)) self.interview.attachment_options['initial_yaml'].append(FileInPackage(yaml_file, 'template', self.package)) elif key == 'additional yaml': if 'additional_yaml' not in self.interview.attachment_options: @@ -2575,30 +2577,30 @@ def __init__(self, orig_data, caller, **kwargs): the_list = [value] for yaml_file in the_list: if not isinstance(yaml_file, str): - raise DAError('An additional yaml file must be a string.' + self.idebug(data)) + raise DASourceError('An additional yaml file must be a string.' + self.idebug(data)) self.interview.attachment_options['additional_yaml'].append(FileInPackage(yaml_file, 'template', self.package)) elif key == 'template file': if not isinstance(value, str): - raise DAError('The template file must be a string.' + self.idebug(data)) + raise DASourceError('The template file must be a string.' + self.idebug(data)) self.interview.attachment_options['template_file'] = FileInPackage(value, 'template', self.package) elif key == 'rtf template file': if not isinstance(value, str): - raise DAError('The rtf template file must be a string.' + self.idebug(data)) + raise DASourceError('The rtf template file must be a string.' + self.idebug(data)) self.interview.attachment_options['rtf_template_file'] = FileInPackage(value, 'template', self.package) elif key == 'docx reference file': if not isinstance(value, str): - raise DAError('The docx reference file must be a string.' + self.idebug(data)) + raise DASourceError('The docx reference file must be a string.' + self.idebug(data)) self.interview.attachment_options['docx_reference_file'] = FileInPackage(value, 'template', self.package) if 'script' in data: if not isinstance(data['script'], str): - raise DAError("A script section must be plain text." + self.idebug(data)) + raise DASourceError("A script section must be plain text." + self.idebug(data)) self.script = TextObject(definitions + DO_NOT_TRANSLATE + str(data['script']), question=self) if 'css' in data: if not isinstance(data['css'], str): - raise DAError("A css section must be plain text." + self.idebug(data)) + raise DASourceError("A css section must be plain text." + self.idebug(data)) self.css = TextObject(definitions + DO_NOT_TRANSLATE + str(data['css']), question=self) if 'initial' in data and 'code' not in data: - raise DAError("Only a code block can be marked as initial." + self.idebug(data)) + raise DASourceError("Only a code block can be marked as initial." + self.idebug(data)) if 'initial' in data or 'default role' in data: if 'default role' in data or data['initial'] is True: self.is_initial = True @@ -2641,10 +2643,10 @@ def __init__(self, orig_data, caller, **kwargs): else: self.other_fields_used.add(key) else: - raise DAError("An objects section cannot contain a nested list." + self.idebug(data)) + raise DASourceError("An objects section cannot contain a nested list." + self.idebug(data)) if 'data' in data and 'variable name' in data: if not isinstance(data['variable name'], str): - raise DAError("A data block variable name must be plain text." + self.idebug(data)) + raise DASourceError("A data block variable name must be plain text." + self.idebug(data)) if self.scan_for_variables: self.fields_used.add(data['variable name'].strip()) else: @@ -2669,7 +2671,7 @@ def __init__(self, orig_data, caller, **kwargs): self.fields.append(Field({'saveas': data['variable name'].strip(), 'type': 'data', 'data': self.recursive_dataobject(data['data'])})) if 'data from code' in data and 'variable name' in data: if not isinstance(data['variable name'], str): - raise DAError("A data from code block variable name must be plain text." + self.idebug(data)) + raise DASourceError("A data from code block variable name must be plain text." + self.idebug(data)) if self.scan_for_variables: self.fields_used.add(data['variable name']) else: @@ -2695,7 +2697,7 @@ def __init__(self, orig_data, caller, **kwargs): if 'objects' in data: if not isinstance(data['objects'], list): data['objects'] = [data['objects']] - # raise DAError("An objects section must be organized as a list." + self.idebug(data)) + # raise DASourceError("An objects section must be organized as a list." + self.idebug(data)) self.question_type = 'objects' self.objects = data['objects'] for item in data['objects']: @@ -2707,10 +2709,10 @@ def __init__(self, orig_data, caller, **kwargs): else: self.other_fields_used.add(key) else: - raise DAError("An objects section cannot contain a nested list." + self.idebug(data)) + raise DASourceError("An objects section cannot contain a nested list." + self.idebug(data)) if 'id' in data: # if str(data['id']) in self.interview.ids_in_use: - # raise DAError("The id " + str(data['id']) + " is already in use by another block. Id names must be unique." + self.idebug(data)) + # raise DASourceError("The id " + str(data['id']) + " is already in use by another block. Id names must be unique." + self.idebug(data)) self.id = str(data['id']).strip() if self.interview.debug and self.interview.source.package.startswith('docassemble.playground') and self.id in self.interview.ids_in_use: self.interview.issue['id_collision'] = self.id @@ -2718,31 +2720,31 @@ def __init__(self, orig_data, caller, **kwargs): self.interview.questions_by_id[self.id] = self if 'ga id' in data: if not isinstance(data['ga id'], str): - raise DAError("A 'ga id' must refer to text." + self.idebug(data)) + raise DASourceError("A 'ga id' must refer to text." + self.idebug(data)) self.ga_id = TextObject(definitions + str(data['ga id']), question=self) if 'segment id' in data: if not isinstance(data['segment id'], str): - raise DAError("A 'segment id' must refer to text." + self.idebug(data)) + raise DASourceError("A 'segment id' must refer to text." + self.idebug(data)) if not hasattr(self, 'segment'): self.segment = {'arguments': {}} self.segment['id'] = TextObject(definitions + str(data['segment id']), question=self) if 'segment' in data: if not isinstance(data['segment'], dict): - raise DAError("A 'segment' must refer to a dictionary." + self.idebug(data)) + raise DASourceError("A 'segment' must refer to a dictionary." + self.idebug(data)) if 'id' in data['segment']: if not isinstance(data['segment']['id'], str): - raise DAError("An 'id' under 'segment' must refer to text." + self.idebug(data)) + raise DASourceError("An 'id' under 'segment' must refer to text." + self.idebug(data)) if not hasattr(self, 'segment'): self.segment = {'arguments': {}} self.segment['id'] = TextObject(definitions + str(data['segment']['id']), question=self) if 'arguments' in data['segment']: if not isinstance(data['segment']['arguments'], dict): - raise DAError("An 'arguments' under 'segment' must refer to a dictionary." + self.idebug(data)) + raise DASourceError("An 'arguments' under 'segment' must refer to a dictionary." + self.idebug(data)) if not hasattr(self, 'segment'): self.segment = {'arguments': {}} for key, val in data['segment']['arguments'].items(): if not isinstance(val, (str, int, float, bool)): - raise DAError("Each item under 'arguments' in a 'segment' must be plain text." + self.idebug(data)) + raise DASourceError("Each item under 'arguments' in a 'segment' must be plain text." + self.idebug(data)) self.segment['arguments'][key] = TextObject(definitions + str(val), question=self) if 'supersedes' in data: if not isinstance(data['supersedes'], list): @@ -2753,16 +2755,16 @@ def __init__(self, orig_data, caller, **kwargs): if 'order' in data: should_append = False if 'question' in data or 'code' in data or 'attachment' in data or 'attachments' in data or 'template' in data: - raise DAError("An 'order' block cannot be combined with another type of block." + self.idebug(data)) + raise DASourceError("An 'order' block cannot be combined with another type of block." + self.idebug(data)) if not isinstance(data['order'], list): - raise DAError("An 'order' block must be a list." + self.idebug(data)) + raise DASourceError("An 'order' block must be a list." + self.idebug(data)) self.interview.id_orderings.append({'type': "order", 'order': [str(x) for x in data['order']]}) for key in ('image sets', 'images'): if key not in data: continue should_append = False if not isinstance(data[key], dict): - raise DAError("The '" + key + "' section needs to be a dictionary, not a list or text." + self.idebug(data)) + raise DASourceError("The '" + key + "' section needs to be a dictionary, not a list or text." + self.idebug(data)) if key == 'images': data[key] = {'unspecified': {'images': data[key]}} elif 'images' in data[key] and 'attribution' in data[key]: @@ -2770,11 +2772,11 @@ def __init__(self, orig_data, caller, **kwargs): for setname, image_set in data[key].items(): if not isinstance(image_set, dict): if key == 'image sets': - raise DAError("Each item in the 'image sets' section needs to be a dictionary, not a list. Each dictionary item should have an 'images' definition (which can be a dictionary or list) and an optional 'attribution' definition (which must be text)." + self.idebug(data)) - raise DAError("Each item in the 'images' section needs to be a dictionary, not a list." + self.idebug(data)) + raise DASourceError("Each item in the 'image sets' section needs to be a dictionary, not a list. Each dictionary item should have an 'images' definition (which can be a dictionary or list) and an optional 'attribution' definition (which must be text)." + self.idebug(data)) + raise DASourceError("Each item in the 'images' section needs to be a dictionary, not a list." + self.idebug(data)) if 'attribution' in image_set: if not isinstance(image_set['attribution'], str): - raise DAError("An attribution in an 'image set' section cannot be a dictionary or a list." + self.idebug(data)) + raise DASourceError("An attribution in an 'image set' section cannot be a dictionary or a list." + self.idebug(data)) attribution = re.sub(r'\n', ' ', image_set['attribution'].strip()) else: attribution = None @@ -2785,8 +2787,8 @@ def __init__(self, orig_data, caller, **kwargs): image_list = [image_set['images']] else: if key == 'image set': - raise DAError("An 'images' definition in an 'image set' item must be a dictionary or a list." + self.idebug(data)) - raise DAError("An 'images' section must be a dictionary or a list." + self.idebug(data)) + raise DASourceError("An 'images' definition in an 'image set' item must be a dictionary or a list." + self.idebug(data)) + raise DASourceError("An 'images' section must be a dictionary or a list." + self.idebug(data)) for image in image_list: if not isinstance(image, dict): the_image = {str(image): str(image)} @@ -2797,7 +2799,7 @@ def __init__(self, orig_data, caller, **kwargs): if 'def' in data: should_append = False if not isinstance(data['def'], str): - raise DAError("A def name must be a string." + self.idebug(data)) + raise DASourceError("A def name must be a string." + self.idebug(data)) if data['def'] not in self.interview.defs: self.interview.defs[data['def']] = [] if 'mako' in data: @@ -2806,15 +2808,15 @@ def __init__(self, orig_data, caller, **kwargs): elif isinstance(data['mako'], list): list_of_defs = data['mako'] else: - raise DAError("A mako template definition must be a string or a list of strings." + self.idebug(data)) + raise DASourceError("A mako template definition must be a string or a list of strings." + self.idebug(data)) for definition in list_of_defs: if not isinstance(definition, str): - raise DAError("A mako template definition must be a string." + self.idebug(data)) + raise DASourceError("A mako template definition must be a string." + self.idebug(data)) self.interview.defs[data['def']].append(definition) if 'interview help' in data: should_append = False if isinstance(data['interview help'], list): - raise DAError("An interview help section must not be in the form of a list." + self.idebug(data)) + raise DASourceError("An interview help section must not be in the form of a list." + self.idebug(data)) if not isinstance(data['interview help'], dict): data['interview help'] = {'content': str(data['interview help'])} audiovideo = [] @@ -2828,7 +2830,7 @@ def __init__(self, orig_data, caller, **kwargs): audiovideo = [] for the_item in the_list: if isinstance(the_item, (list, dict)): - raise DAError("An interview help audio section must be in the form of a text item or a list of text items." + self.idebug(data)) + raise DASourceError("An interview help audio section must be in the form of a text item or a list of text items." + self.idebug(data)) audiovideo.append({'text': TextObject(definitions + str(data['interview help']['audio']), question=self), 'package': self.package, 'type': 'audio'}) if 'video' in data['interview help']: if not isinstance(data['interview help']['video'], list): @@ -2837,7 +2839,7 @@ def __init__(self, orig_data, caller, **kwargs): the_list = data['interview help']['video'] for the_item in the_list: if isinstance(the_item, (list, dict)): - raise DAError("An interview help video section must be in the form of a text item or a list of text items." + self.idebug(data)) + raise DASourceError("An interview help video section must be in the form of a text item or a list of text items." + self.idebug(data)) audiovideo.append({'text': TextObject(definitions + str(data['interview help']['video']), question=self), 'package': self.package, 'type': 'video'}) if 'video' not in data['interview help'] and 'audio' not in data['interview help']: audiovideo = None @@ -2845,21 +2847,21 @@ def __init__(self, orig_data, caller, **kwargs): if not isinstance(data['interview help']['heading'], (dict, list)): help_heading = TextObject(definitions + str(data['interview help']['heading']), question=self) else: - raise DAError("A heading within an interview help section must be text, not a list or a dictionary." + self.idebug(data)) + raise DASourceError("A heading within an interview help section must be text, not a list or a dictionary." + self.idebug(data)) else: help_heading = None if 'content' in data['interview help']: if not isinstance(data['interview help']['content'], (dict, list)): help_content = TextObject(definitions + str(data['interview help']['content']), question=self) else: - raise DAError("Help content must be text, not a list or a dictionary." + self.idebug(data)) + raise DASourceError("Help content must be text, not a list or a dictionary." + self.idebug(data)) else: - raise DAError("No content section was found in an interview help section." + self.idebug(data)) + raise DASourceError("No content section was found in an interview help section." + self.idebug(data)) if 'label' in data['interview help']: if not isinstance(data['interview help']['label'], (dict, list)): help_label = TextObject(definitions + str(data['interview help']['label']), question=self) else: - raise DAError("Help label must be text, not a list or a dictionary." + self.idebug(data)) + raise DASourceError("Help label must be text, not a list or a dictionary." + self.idebug(data)) else: help_label = None if self.language not in self.interview.helptext: @@ -2868,7 +2870,7 @@ def __init__(self, orig_data, caller, **kwargs): if 'default screen parts' in data: should_append = False if not isinstance(data['default screen parts'], dict): - raise DAError("A default screen parts block must be in the form of a dictionary." + self.idebug(data)) + raise DASourceError("A default screen parts block must be in the form of a dictionary." + self.idebug(data)) if self.language not in self.interview.default_screen_parts: self.interview.default_screen_parts[self.language] = {} for key, content in data['default screen parts'].items(): @@ -2877,17 +2879,17 @@ def __init__(self, orig_data, caller, **kwargs): del self.interview.default_screen_parts[self.language][key] else: if not (isinstance(key, str) and isinstance(content, str)): - raise DAError("A default screen parts block must be a dictionary of text keys and text values." + self.idebug(data)) + raise DASourceError("A default screen parts block must be a dictionary of text keys and text values." + self.idebug(data)) self.interview.default_screen_parts[self.language][key] = TextObject(definitions + str(content.strip()), question=self) if 'default validation messages' in data: should_append = False if not isinstance(data['default validation messages'], dict): - raise DAError("A default validation messages block must be in the form of a dictionary." + self.idebug(data)) + raise DASourceError("A default validation messages block must be in the form of a dictionary." + self.idebug(data)) if self.language not in self.interview.default_validation_messages: self.interview.default_validation_messages[self.language] = {} for validation_key, validation_message in data['default validation messages'].items(): if not (isinstance(validation_key, str) and isinstance(validation_message, str)): - raise DAError("A validation messages block must be a dictionary of text keys and text values." + self.idebug(data)) + raise DASourceError("A validation messages block must be a dictionary of text keys and text values." + self.idebug(data)) self.interview.default_validation_messages[self.language][validation_key] = validation_message.strip() if 'generic object' in data: self.is_generic = True @@ -2904,7 +2906,7 @@ def __init__(self, orig_data, caller, **kwargs): if 'metadata' in data: for key in data: if key not in ('metadata', 'comment'): - raise DAError("A metadata directive cannot be mixed with other directives." + self.idebug(data)) + raise DASourceError("A metadata directive cannot be mixed with other directives." + self.idebug(data)) should_append = False if isinstance(data['metadata'], dict): data['metadata']['_origin_path'] = self.from_source.path @@ -2913,7 +2915,7 @@ def __init__(self, orig_data, caller, **kwargs): self.interview.default_language = data['metadata']['default language'] self.interview.metadata.append(data['metadata']) else: - raise DAError("A metadata section must be organized as a dictionary." + self.idebug(data)) + raise DASourceError("A metadata section must be organized as a dictionary." + self.idebug(data)) if 'modules' in data: if isinstance(data['modules'], str): data['modules'] = [data['modules']] @@ -2926,7 +2928,7 @@ def __init__(self, orig_data, caller, **kwargs): self.question_type = 'modules' self.module_list = data['modules'] else: - raise DAError("A modules section must be organized as a list." + self.idebug(data)) + raise DASourceError("A modules section must be organized as a list." + self.idebug(data)) if 'reset' in data: # logmessage("Found a reset") if isinstance(data['reset'], str): @@ -2935,7 +2937,7 @@ def __init__(self, orig_data, caller, **kwargs): self.question_type = 'reset' self.reset_list = data['reset'] else: - raise DAError("A reset section must be organized as a list." + self.idebug(data)) + raise DASourceError("A reset section must be organized as a list." + self.idebug(data)) if 'imports' in data: if isinstance(data['imports'], str): data['imports'] = [data['imports']] @@ -2943,16 +2945,16 @@ def __init__(self, orig_data, caller, **kwargs): self.question_type = 'imports' self.module_list = data['imports'] else: - raise DAError("An imports section must be organized as a list." + self.idebug(data)) + raise DASourceError("An imports section must be organized as a list." + self.idebug(data)) if 'terms' in data and 'question' in data: if not isinstance(data['terms'], (dict, list)): - raise DAError("Terms must be organized as a dictionary or a list." + self.idebug(data)) + raise DASourceError("Terms must be organized as a dictionary or a list." + self.idebug(data)) if isinstance(data['terms'], dict): data['terms'] = [data['terms']] for termitem in data['terms']: if not isinstance(termitem, dict): - raise DAError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data)) + raise DASourceError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data)) if len(termitem) == 2 and 'phrases' in termitem and isinstance(termitem['phrases'], list) and 'definition' in termitem: termitems = [(phrase, termitem['definition']) for phrase in termitem['phrases']] else: @@ -2970,12 +2972,12 @@ def __init__(self, orig_data, caller, **kwargs): self.terms[lower_term] = {'definition': TextObject(definitions + str(definition), question=self), 're': re_dict, 'alt_terms': alt_terms} if 'auto terms' in data and 'question' in data: if not isinstance(data['auto terms'], (dict, list)): - raise DAError("Terms must be organized as a dictionary or a list." + self.idebug(data)) + raise DASourceError("Terms must be organized as a dictionary or a list." + self.idebug(data)) if isinstance(data['auto terms'], dict): data['auto terms'] = [data['auto terms']] for termitem in data['auto terms']: if not isinstance(termitem, dict): - raise DAError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data)) + raise DASourceError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data)) if len(termitem) == 2 and 'phrases' in termitem and isinstance(termitem['phrases'], list) and 'definition' in termitem: termitems = [(phrase, termitem['definition']) for phrase in termitem['phrases']] else: @@ -3015,7 +3017,7 @@ def __init__(self, orig_data, caller, **kwargs): lower_other = re.sub(r'\s+', ' ', tr_tuple[0].lower()) self.interview.terms[lang][tr_tuple[0]] = {'definition': definition_textobject.other_lang[lang][0], 're': re.compile(r"{(?i)(%s)(\|[^\}]*)?}" % (re.sub(r'\s', '\\\s+', lower_other),), re.IGNORECASE | re.DOTALL)} # noqa: W605 else: - raise DAError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data)) + raise DASourceError("A terms section organized as a list must be a list of dictionary items." + self.idebug(data)) elif isinstance(data['terms'], dict): for term in data['terms']: lower_term = re.sub(r'\s+', ' ', term.lower()) @@ -3030,7 +3032,7 @@ def __init__(self, orig_data, caller, **kwargs): lower_other = re.sub(r'\s+', ' ', tr_tuple[0].lower()) self.interview.terms[lang][tr_tuple[0]] = {'definition': definition_textobject.other_lang[lang][0], 're': re.compile(r"{(?i)(%s)(\|[^\}]*)?}" % (re.sub(r'\s', '\\\s+', lower_other),), re.IGNORECASE | re.DOTALL)} # noqa: W605 else: - raise DAError("A terms section must be organized as a dictionary or a list." + self.idebug(data)) + raise DASourceError("A terms section must be organized as a dictionary or a list." + self.idebug(data)) if 'auto terms' in data and 'question' not in data: should_append = False if self.language not in self.interview.autoterms: @@ -3055,7 +3057,7 @@ def __init__(self, orig_data, caller, **kwargs): lower_other = re.sub(r'\s+', ' ', tr_tuple[0].lower()) self.interview.autoterms[lang][tr_tuple[0]] = {'definition': definition_textobject.other_lang[lang][0], 're': re.compile(r"{?(?i)\b(%s)\b}?" % (re.sub(r'\s', '\\\s+', lower_other),), re.IGNORECASE | re.DOTALL)} # noqa: W605 else: - raise DAError("An auto terms section organized as a list must be a list of dictionary items." + self.idebug(data)) + raise DASourceError("An auto terms section organized as a list must be a list of dictionary items." + self.idebug(data)) elif isinstance(data['auto terms'], dict): for term in data['auto terms']: lower_term = re.sub(r'\s+', ' ', term.lower()) @@ -3070,7 +3072,7 @@ def __init__(self, orig_data, caller, **kwargs): lower_other = re.sub(r'\s+', ' ', tr_tuple[0].lower()) self.interview.autoterms[lang][tr_tuple[0]] = {'definition': definition_textobject.other_lang[lang][0], 're': re.compile(r"{?(?i)\b(%s)\b}?" % (re.sub(r'\s', '\\\s+', lower_other),), re.IGNORECASE | re.DOTALL)} # noqa: W605 else: - raise DAError("An auto terms section must be organized as a dictionary or a list." + self.idebug(data)) + raise DASourceError("An auto terms section must be organized as a dictionary or a list." + self.idebug(data)) if 'default role' in data: if 'code' not in data: should_append = False @@ -3079,7 +3081,7 @@ def __init__(self, orig_data, caller, **kwargs): elif isinstance(data['default role'], list): self.interview.default_role = data['default role'] else: - raise DAError("A default role must be a list or a string." + self.idebug(data)) + raise DASourceError("A default role must be a list or a string." + self.idebug(data)) if 'role' in data: if isinstance(data['role'], str): if data['role'] not in self.role: @@ -3089,7 +3091,7 @@ def __init__(self, orig_data, caller, **kwargs): if data['role'] not in self.role: self.role.append(rolename) else: - raise DAError("The role of a question must be a string or a list." + self.idebug(data)) + raise DASourceError("The role of a question must be a string or a list." + self.idebug(data)) else: self.role = [] if 'include' in data: @@ -3109,19 +3111,19 @@ def __init__(self, orig_data, caller, **kwargs): 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'], '', 'eval')) self.find_fields_in(data['action buttons']['code']) else: if not isinstance(data['action buttons'], list): - raise DAError("An action buttons specifier must be a list." + self.idebug(data)) + raise DASourceError("An action buttons specifier must be a list." + self.idebug(data)) for item in data['action buttons']: if not isinstance(item, dict): - raise DAError("An action buttons item must be a dictionary." + self.idebug(data)) + raise DASourceError("An action buttons item must be a dictionary." + self.idebug(data)) action = item.get('action', None) target = item.get('new window', None) if target is True: @@ -3141,23 +3143,23 @@ def __init__(self, orig_data, caller, **kwargs): forget_prior = item.get('forget prior', False) given_arguments = item.get('arguments', {}) if not isinstance(action, str): - raise DAError("An action buttons item must contain an action in plain text." + self.idebug(data)) + raise DASourceError("An action buttons item must contain an action in plain text." + self.idebug(data)) if not isinstance(target, (str, NoneType)): - raise DAError("The new window specifier in an action buttons item must refer to True or plain text." + self.idebug(data)) + raise DASourceError("The new window specifier in an action buttons item must refer to True or plain text." + self.idebug(data)) if not isinstance(given_arguments, dict): - raise DAError("The arguments specifier in an action buttons item must refer to a dictionary." + self.idebug(data)) + raise DASourceError("The arguments specifier in an action buttons item must refer to a dictionary." + self.idebug(data)) if not isinstance(label, str): - raise DAError("An action buttons item must contain a label in plain text." + self.idebug(data)) + raise DASourceError("An action buttons item must contain a label in plain text." + self.idebug(data)) if not isinstance(color, str): - raise DAError("The color specifier in an action buttons item must refer to plain text." + self.idebug(data)) + raise DASourceError("The color specifier in an action buttons item must refer to plain text." + self.idebug(data)) if not isinstance(icon, (str, NoneType)): - raise DAError("The icon specifier in an action buttons item must refer to plain text." + self.idebug(data)) + raise DASourceError("The icon specifier in an action buttons item must refer to plain text." + self.idebug(data)) if not isinstance(placement, (str, NoneType)): - raise DAError("The placement specifier in an action buttons item must refer to plain text." + self.idebug(data)) + raise DASourceError("The placement specifier in an action buttons item must refer to plain text." + self.idebug(data)) if not isinstance(css_class, (str, NoneType)): - raise DAError("The css classifier specifier in an action buttons item must refer to plain text." + self.idebug(data)) + raise DASourceError("The css classifier specifier in an action buttons item must refer to plain text." + self.idebug(data)) if not isinstance(forget_prior, bool): - raise DAError("The forget prior specifier in an action buttons item must refer to true or false." + self.idebug(data)) + raise DASourceError("The forget prior specifier in an action buttons item must refer to true or false." + self.idebug(data)) button = {'action': TextObject(definitions + action, question=self), 'label': TextObject(definitions + label, question=self), 'color': TextObject(definitions + color, question=self)} button['show if'] = showif if target is not None: @@ -3183,7 +3185,7 @@ def __init__(self, orig_data, caller, **kwargs): button['arguments'] = {} for key, val in given_arguments.items(): if isinstance(val, (list, dict)): - raise DAError("The arguments specifier in an action buttons item must refer to plain items." + self.idebug(data)) + raise DASourceError("The arguments specifier in an action buttons item must refer to plain items." + self.idebug(data)) if isinstance(val, str): button['arguments'][key] = TextObject(definitions + val, question=self) else: @@ -3198,10 +3200,10 @@ def __init__(self, orig_data, caller, **kwargs): 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'', 'exec') self.find_fields_in(data['validation code']) if 'require' in data: @@ -3218,11 +3220,11 @@ def __init__(self, orig_data, caller, **kwargs): if isinstance(data['orelse'], dict): self.or_else_question = Question(data['orelse'], self.interview, register_target=register_target, source=self.from_source, package=self.package) else: - raise DAError("The orelse part of a require section must be organized as a dictionary." + self.idebug(data)) + raise DASourceError("The orelse part of a require section must be organized as a dictionary." + self.idebug(data)) else: - raise DAError("A require section must have an orelse part." + self.idebug(data)) + raise DASourceError("A require section must have an orelse part." + self.idebug(data)) else: - raise DAError("A require section must be organized as a list." + self.idebug(data)) + raise DASourceError("A require section must be organized as a list." + self.idebug(data)) if 'attachment' in data: self.attachments = self.process_attachment_list(data['attachment']) elif 'attachments' in data: @@ -3260,7 +3262,7 @@ def __init__(self, orig_data, caller, **kwargs): # elif isinstance(data['role'], str) and data['role'] not in self.role: # self.role.append(data['role']) # else: - # raise DAError("A role section must be text or a list." + self.idebug(data)) + # raise DASourceError("A role section must be text or a list." + self.idebug(data)) if 'progress' in data: if data['progress'] is None: self.progress = -1 @@ -3338,11 +3340,11 @@ def __init__(self, orig_data, caller, **kwargs): self.response_code = data['response code'] if 'css class' in data: if 'question' not in data: - raise DAError("A css class can only accompany a question." + self.idebug(data)) + raise DASourceError("A css class can only accompany a question." + self.idebug(data)) self.css_class = TextObject(definitions + str(data['css class']), question=self) if 'table css class' in data: if 'question' not in data: - raise DAError("A table css class can only accompany a question." + self.idebug(data)) + raise DASourceError("A table css class can only accompany a question." + self.idebug(data)) self.table_css_class = TextObject(definitions + str(data['table css class']), question=self) if 'question' in data: self.content = TextObject(definitions + str(data['question']), question=self) @@ -3366,7 +3368,7 @@ def __init__(self, orig_data, caller, **kwargs): the_list = value for list_item in the_list: if isinstance(list_item, (dict, list, set)): - raise DAError("An audio declaration in a help block can only contain a text item or a list of text items." + self.idebug(data)) + raise DASourceError("An audio declaration in a help block can only contain a text item or a list of text items." + self.idebug(data)) if self.audiovideo is None: self.audiovideo = {} if 'help' not in self.audiovideo: @@ -3379,7 +3381,7 @@ def __init__(self, orig_data, caller, **kwargs): the_list = value for list_item in the_list: if isinstance(list_item, (dict, list, set)): - raise DAError("A video declaration in a help block can only contain a text item or a list of text items." + self.idebug(data)) + raise DASourceError("A video declaration in a help block can only contain a text item or a list of text items." + self.idebug(data)) if self.audiovideo is None: self.audiovideo = {} if 'help' not in self.audiovideo: @@ -3387,7 +3389,7 @@ def __init__(self, orig_data, caller, **kwargs): self.audiovideo['help'].append({'text': TextObject(definitions + str(list_item.strip()), question=self), 'package': self.package, 'type': 'video'}) if key == 'content': if isinstance(value, (dict, list, set)): - raise DAError("A content declaration in a help block can only contain text." + self.idebug(data)) + raise DASourceError("A content declaration in a help block can only contain text." + self.idebug(data)) self.helptext = TextObject(definitions + str(value), question=self) else: self.helptext = TextObject(definitions + str(data['help']), question=self) @@ -3398,7 +3400,7 @@ def __init__(self, orig_data, caller, **kwargs): the_list = data['audio'] for list_item in the_list: if isinstance(list_item, (dict, list, set)): - raise DAError("An audio declaration can only contain a text item or a list of text items." + self.idebug(data)) + raise DASourceError("An audio declaration can only contain a text item or a list of text items." + self.idebug(data)) if self.audiovideo is None: self.audiovideo = {} if 'question' not in self.audiovideo: @@ -3411,7 +3413,7 @@ def __init__(self, orig_data, caller, **kwargs): the_list = data['video'] for list_item in the_list: if isinstance(list_item, (dict, list, set)): - raise DAError("A video declaration can only contain a text item or a list of text items." + self.idebug(data)) + raise DASourceError("A video declaration can only contain a text item or a list of text items." + self.idebug(data)) if self.audiovideo is None: self.audiovideo = {} if 'question' not in self.audiovideo: @@ -3455,7 +3457,7 @@ def __init__(self, orig_data, caller, **kwargs): else: self.other_fields_used.add(data['signature']) elif 'required' in data: - raise DAError("The required modifier can only be used on a signature block" + self.idebug(data)) + raise DASourceError("The required modifier can only be used on a signature block" + self.idebug(data)) if 'question metadata' in data: self.question_metadata = recursive_textobject_or_primitive(data['question metadata'], self) if 'under' in data: @@ -3469,12 +3471,12 @@ def __init__(self, orig_data, caller, **kwargs): if 'check in' in data: self.interview.uses_action = True if isinstance(data['check in'], (dict, list, set)): - raise DAError("A check in event must be text or a list." + self.idebug(data)) + raise DASourceError("A check in event must be text or a list." + self.idebug(data)) self.checkin = str(data['check in']) self.names_used.add(str(data['check in'])) if 'yesno' in data: if not isinstance(data['yesno'], str): - raise DAError("A yesno must refer to text." + self.idebug(data)) + raise DASourceError("A yesno must refer to text." + self.idebug(data)) self.fields.append(Field({'saveas': data['yesno'], 'boolean': 1})) if self.scan_for_variables: self.fields_used.add(data['yesno']) @@ -3483,7 +3485,7 @@ def __init__(self, orig_data, caller, **kwargs): self.question_type = 'yesno' if 'noyes' in data: if not isinstance(data['noyes'], str): - raise DAError("A noyes must refer to text." + self.idebug(data)) + raise DASourceError("A noyes must refer to text." + self.idebug(data)) self.fields.append(Field({'saveas': data['noyes'], 'boolean': -1})) if self.scan_for_variables: self.fields_used.add(data['noyes']) @@ -3492,7 +3494,7 @@ def __init__(self, orig_data, caller, **kwargs): self.question_type = 'noyes' if 'yesnomaybe' in data: if not isinstance(data['yesnomaybe'], str): - raise DAError("A yesnomaybe must refer to text." + self.idebug(data)) + raise DASourceError("A yesnomaybe must refer to text." + self.idebug(data)) self.fields.append(Field({'saveas': data['yesnomaybe'], 'threestate': 1})) if self.scan_for_variables: self.fields_used.add(data['yesnomaybe']) @@ -3501,7 +3503,7 @@ def __init__(self, orig_data, caller, **kwargs): self.question_type = 'yesnomaybe' if 'noyesmaybe' in data: if not isinstance(data['noyesmaybe'], str): - raise DAError("A noyesmaybe must refer to text." + self.idebug(data)) + raise DASourceError("A noyesmaybe must refer to text." + self.idebug(data)) self.fields.append(Field({'saveas': data['noyesmaybe'], 'threestate': -1})) if self.scan_for_variables: self.fields_used.add(data['noyesmaybe']) @@ -3515,7 +3517,7 @@ def __init__(self, orig_data, caller, **kwargs): for key in data['sets']: self.fields_used.add(key) else: - raise DAError("A sets phrase must be text or a list." + self.idebug(data)) + raise DASourceError("A sets phrase must be text or a list." + self.idebug(data)) if 'event' in data: self.interview.uses_action = True if isinstance(data['event'], str): @@ -3524,20 +3526,20 @@ def __init__(self, orig_data, caller, **kwargs): for key in data['event']: self.fields_used.add(key) else: - raise DAError("An event phrase must be text or a list." + self.idebug(data)) + raise DASourceError("An event phrase must be text or a list." + self.idebug(data)) if 'choices' in data or 'buttons' in data or 'dropdown' in data or 'combobox' in data: if 'field' in data: uses_field = True uses_continue_button_field = False data['field'] = data['field'].strip() if invalid_variable_name(data['field']): - raise DAError("Missing or invalid variable name " + repr(data['field']) + "." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(data['field']) + "." + self.idebug(data)) else: uses_field = False if 'continue button field' in data: data['continue button field'] = data['continue button field'].strip() if invalid_variable_name(data['continue button field']): - raise DAError("Missing or invalid variable name " + repr(data['continue button field']) + "." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(data['continue button field']) + "." + self.idebug(data)) uses_continue_button_field = True else: uses_continue_button_field = False @@ -3565,11 +3567,11 @@ def __init__(self, orig_data, caller, **kwargs): self.question_variety = 'buttons' if 'validation messages' in data: if not isinstance(data['validation messages'], dict): - raise DAError("A validation messages indicator must be a dictionary." + self.idebug(data)) + raise DASourceError("A validation messages indicator must be a dictionary." + self.idebug(data)) field_data['validation messages'] = {} for validation_key, validation_message in data['validation messages'].items(): if not (isinstance(validation_key, str) and isinstance(validation_message, str)): - raise DAError("A validation messages indicator must be a dictionary of text keys and text values." + self.idebug(data)) + raise DASourceError("A validation messages indicator must be a dictionary of text keys and text values." + self.idebug(data)) field_data['validation messages'][validation_key] = TextObject(definitions + str(validation_message).strip(), question=self) if uses_field: if self.scan_for_variables: @@ -3597,7 +3599,7 @@ def __init__(self, orig_data, caller, **kwargs): self.question_type = 'multiple_choice' elif 'continue button field' in data and 'fields' not in data and 'yesno' not in data and 'noyes' not in data and 'yesnomaybe' not in data and 'noyesmaybe' not in data and 'signature' not in data: if not isinstance(data['continue button field'], str): - raise DAError("A continue button field must be plain text." + self.idebug(data)) + raise DASourceError("A continue button field must be plain text." + self.idebug(data)) if self.scan_for_variables: self.fields_used.add(data['continue button field']) else: @@ -3614,32 +3616,32 @@ def __init__(self, orig_data, caller, **kwargs): 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, '', 'eval')), pre_need_list)) @@ -3670,18 +3672,18 @@ def __init__(self, orig_data, caller, **kwargs): 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'], '', 'eval') self.find_fields_in(data['rows']) header = [] @@ -3697,9 +3699,9 @@ def __init__(self, orig_data, caller, **kwargs): not_available_label = 'n/a' for col in data['columns']: if not isinstance(col, dict): - raise DAError("The column items in a table definition must be dictionaries." + self.idebug(data)) + raise DASourceError("The column items in a table definition must be dictionaries." + self.idebug(data)) if len(col) == 0: - raise DAError("A column item in a table definition cannot be empty." + self.idebug(data)) + raise DASourceError("A column item in a table definition cannot be empty." + self.idebug(data)) if 'header' in col and 'cell' in col: header_text = col['header'] cell_text = str(col['cell']).strip() @@ -3719,12 +3721,12 @@ def __init__(self, orig_data, caller, **kwargs): is_editable = True if isinstance(data['edit'], list): if len(data['edit']) == 0: - raise DAError("The edit directive must be a list of attributes, or True or False" + self.idebug(data)) + raise DASourceError("The edit directive must be a list of attributes, or True or False" + self.idebug(data)) for attribute_name in data['edit']: if not isinstance(attribute_name, str): - raise DAError("The edit directive must be a list of attribute names" + self.idebug(data)) + raise DASourceError("The edit directive must be a list of attribute names" + self.idebug(data)) elif not isinstance(data['edit'], bool): - raise DAError("The edit directive must be a list of attributes, or True or False" + self.idebug(data)) + raise DASourceError("The edit directive must be a list of attributes, or True or False" + self.idebug(data)) keyword_args = '' if 'delete buttons' in data and not data['delete buttons']: keyword_args += ', delete=False' @@ -3732,7 +3734,7 @@ def __init__(self, orig_data, caller, **kwargs): 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) + ')', '', 'eval')) @@ -3740,7 +3742,7 @@ def __init__(self, orig_data, caller, **kwargs): column.append(compile('(' + data['rows'] + ').item_actions(row_item, row_index' + keyword_args + ', reorder=' + repr(reorder) + ', ensure_complete=' + repr(ensure_complete) + ')', '', '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(' ')) else: @@ -3752,7 +3754,7 @@ def __init__(self, orig_data, caller, **kwargs): keyword_args = '' if 'read only' in data: if not isinstance(data['read only'], str): - raise DAError("The read only directive must be plain text referring to an attribute" + self.idebug(data)) + raise DASourceError("The read only directive must be plain text referring to an attribute" + self.idebug(data)) keyword_args += ', read_only_attribute=' + repr(data['read only'].strip()) if 'confirm' in data and data['confirm']: keyword_args += ', confirm=True' @@ -3762,7 +3764,7 @@ def __init__(self, orig_data, caller, **kwargs): 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) + ')', '', '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(' ')) else: @@ -3795,27 +3797,27 @@ def __init__(self, orig_data, caller, **kwargs): self.find_fields_in(data['content file']['code']) self.question_type = 'template_code' else: - raise DAError('A content file must be specified as text, as a list of text filenames, or as a dictionary with code as the key' + self.idebug(data)) + raise DASourceError('A content file must be specified as text, as a list of text filenames, or as a dictionary with code as the key' + self.idebug(data)) else: if not isinstance(data['content file'], list): data['content file'] = [data['content file']] data['content'] = '' for content_file in data['content file']: if not isinstance(content_file, str): - raise DAError('A content file must be specified as text, as a list of text filenames, or as a dictionary with code as the key' + self.idebug(data)) + raise DASourceError('A content file must be specified as text, as a list of text filenames, or as a dictionary with code as the key' + self.idebug(data)) file_to_read = docassemble.base.functions.package_template_filename(content_file, package=self.package) # if file_to_read is not None and get_mimetype(file_to_read) != 'text/markdown': - # raise DAError('The content file ' + str(data['content file']) + ' is not a markdown file ' + str(file_to_read) + self.idebug(data)) + # raise DASourceError('The content file ' + str(data['content file']) + ' is not a markdown file ' + str(file_to_read) + self.idebug(data)) if file_to_read is not None and os.path.isfile(file_to_read) and os.access(file_to_read, os.R_OK): with open(file_to_read, 'r', encoding='utf-8') as the_file: data['content'] += the_file.read() else: - raise DAError('Unable to read content file ' + str(data['content file']) + ' after trying to find it at ' + str(file_to_read) + self.idebug(data)) + raise DASourceError('Unable to read content file ' + str(data['content file']) + ' after trying to find it at ' + str(file_to_read) + self.idebug(data)) if 'template' in data and 'content' in data: if isinstance(data['template'], (list, dict)): - raise DAError("A template must designate a single variable expressed as text." + self.idebug(data)) + raise DASourceError("A template must designate a single variable expressed as text." + self.idebug(data)) if isinstance(data['content'], (list, dict)): - raise DAError("The content of a template must be expressed as text." + self.idebug(data)) + raise DASourceError("The content of a template must be expressed as text." + self.idebug(data)) if self.scan_for_variables: self.fields_used.add(data['template']) else: @@ -3848,10 +3850,10 @@ def __init__(self, orig_data, caller, **kwargs): raise self.find_fields_in(data['code']) else: - raise DAError("A code section must be text, not a list or a dictionary." + self.idebug(data)) + raise DASourceError("A code section must be text, not a list or a dictionary." + self.idebug(data)) if 'reconsider' in data: # if not isinstance(data['reconsider'], bool): - # raise DAError("A reconsider directive must be true or false." + self.idebug(data)) + # raise DASourceError("A reconsider directive must be true or false." + self.idebug(data)) if isinstance(data['reconsider'], bool): if data['reconsider']: if self.is_generic: @@ -3866,10 +3868,10 @@ def __init__(self, orig_data, caller, **kwargs): elif isinstance(data['reconsider'], list): fields = data['reconsider'] else: - raise DAError("A reconsider directive must be true, false, a single variable or a list." + self.idebug(data)) + raise DASourceError("A reconsider directive must be true, false, a single variable or a list." + self.idebug(data)) for the_field in fields: if not isinstance(the_field, str): - raise DAError("A reconsider directive must refer to variable names expressed as text." + self.idebug(data)) + raise DASourceError("A reconsider directive must refer to variable names expressed as text." + self.idebug(data)) self.find_fields_in(the_field) self.reconsider.append(the_field) if 'undefine' in data: @@ -3878,15 +3880,15 @@ def __init__(self, orig_data, caller, **kwargs): elif isinstance(data['undefine'], list): fields = data['undefine'] else: - raise DAError("A undefine directive must a single variable or a list." + self.idebug(data)) + raise DASourceError("A undefine directive must a single variable or a list." + self.idebug(data)) for the_field in fields: if not isinstance(the_field, str): - raise DAError("A undefine directive must refer to variable names expressed as text." + self.idebug(data)) + raise DASourceError("A undefine directive must refer to variable names expressed as text." + self.idebug(data)) self.find_fields_in(the_field) self.undefine.append(the_field) if 'continue button field' in data and 'question' in data and ('field' in data or 'fields' in data or 'yesno' in data or 'noyes' in data or 'yesnomaybe' in data or 'noyesmaybe' in data or 'signature' in data): if not isinstance(data['continue button field'], str): - raise DAError("A continue button field must be plain text." + self.idebug(data)) + raise DASourceError("A continue button field must be plain text." + self.idebug(data)) if self.scan_for_variables: self.fields_used.add(data['continue button field']) else: @@ -3897,12 +3899,12 @@ def __init__(self, orig_data, caller, **kwargs): if isinstance(data['fields'], dict): data['fields'] = [data['fields']] if not isinstance(data['fields'], list): - raise DAError("The fields must be written in the form of a list." + self.idebug(data)) + raise DASourceError("The fields must be written in the form of a list." + self.idebug(data)) field_number = 0 for field in data['fields']: docassemble.base.functions.this_thread.misc['current_field'] = field_number if not isinstance(field, dict): - raise DAError("Each individual field in a list of fields must be expressed as a dictionary item, e.g., ' - Fruit: user.favorite_fruit'." + self.idebug(data)) + raise DASourceError("Each individual field in a list of fields must be expressed as a dictionary item, e.g., ' - Fruit: user.favorite_fruit'." + self.idebug(data)) manual_keys = set() field_info = {'type': 'text', 'number': field_number} custom_data_type = False @@ -3910,7 +3912,7 @@ def __init__(self, orig_data, caller, **kwargs): if 'field' in field and 'label' not in field: field['label'] = 'hidden' if field.get('datatype', None) in ['file', 'files', 'camera', 'user', 'environment', 'camcorder', 'microphone']: - raise DAError("Invalid datatype of hidden field." + self.idebug(data)) + raise DASourceError("Invalid datatype of hidden field." + self.idebug(data)) if 'choices' in field and isinstance(field['choices'], dict) and len(field['choices']) == 1 and 'code' in field['choices']: field['code'] = field['choices']['code'] del field['choices'] @@ -3928,18 +3930,18 @@ def __init__(self, orig_data, caller, **kwargs): field['input type'] = 'hidden' field['datatype'] = 'text' if field['datatype'] in ('object', 'object_radio', 'multiselect', 'object_multiselect', 'checkboxes', 'object_checkboxes') and not ('choices' in field or 'code' in field): - raise DAError("A multiple choice field must refer to a list of choices." + self.idebug(data)) + raise DASourceError("A multiple choice field must refer to a list of choices." + self.idebug(data)) if field['datatype'] in docassemble.base.functions.custom_types and field['datatype'] not in standard_types: custom_data_type = True self.interview.custom_data_types.add(field['datatype']) if 'input type' in field: if field['input type'] == 'ajax': if 'action' not in field: - raise DAError("An ajax field must have an associated action." + self.idebug(data)) + raise DASourceError("An ajax field must have an associated action." + self.idebug(data)) if 'choices' in field or 'code' in field: - raise DAError("An ajax field cannot contain a list of choices except through an action." + self.idebug(data)) + raise DASourceError("An ajax field cannot contain a list of choices except through an action." + self.idebug(data)) if field['input type'] in ('radio', 'combobox', 'pulldown') and not ('choices' in field or 'code' in field): - raise DAError("A multiple choice field must refer to a list of choices." + self.idebug(data)) + raise DASourceError("A multiple choice field must refer to a list of choices." + self.idebug(data)) if len(field) == 1 and 'code' in field: field_info['type'] = 'fields_code' self.find_fields_in(field['code']) @@ -3950,9 +3952,9 @@ def __init__(self, orig_data, caller, **kwargs): del docassemble.base.functions.this_thread.misc['current_field'] continue if 'object labeler' in field and ('datatype' not in field or not field['datatype'].startswith('object')): - raise DAError("An object labeler can only be used with an object data type") + raise DASourceError("An object labeler can only be used with an object data type." + self.idebug(data)) if ('note' in field and 'html' in field) or ('note' in field and 'raw html' in field) or ('html' in field and 'raw html' in field): - raise DAError("You cannot combine note, html, and/or raw html in a single field." + self.idebug(data)) + raise DASourceError("You cannot combine note, html, and/or raw html in a single field." + self.idebug(data)) for key in field: if key == 'default' and 'datatype' in field and field['datatype'] in ('object', 'object_radio', 'object_multiselect', 'object_checkboxes'): continue @@ -3996,11 +3998,11 @@ def __init__(self, orig_data, caller, **kwargs): 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], '', 'eval'), 'sourcecode': field[key]} @@ -4027,7 +4029,7 @@ def __init__(self, orig_data, caller, **kwargs): if isinstance(field[key], list): for item in field[key]: if not isinstance(item, str): - raise DAError("An allow privileges specifier must be a list of plain text items or code." + self.idebug(data)) + raise DASourceError("An allow privileges specifier must be a list of plain text items or code." + self.idebug(data)) field_info['allow_privileges'] = field[key] elif isinstance(field[key], str): field_info['allow_privileges'] = [field[key]] @@ -4035,12 +4037,12 @@ def __init__(self, orig_data, caller, **kwargs): field_info['allow_privileges'] = {'compute': compile(field[key]['code'], '', 'eval'), 'sourcecode': field[key]['code']} self.find_fields_in(field[key]['code']) else: - raise DAError("An allow privileges specifier must be a list of plain text items or code." + self.idebug(data)) + raise DASourceError("An allow privileges specifier must be a list of plain text items or code." + self.idebug(data)) elif key == 'allow users' and 'datatype' in field and field['datatype'] in ('file', 'files', 'camera', 'user', 'environment'): if isinstance(field[key], list): for item in field[key]: if not isinstance(item, (str, int)): - raise DAError("An allow users specifier must be a list of integers and plain text items or code." + self.idebug(data)) + raise DASourceError("An allow users specifier must be a list of integers and plain text items or code." + self.idebug(data)) field_info['allow_users'] = field[key] elif isinstance(field[key], str): field_info['allow_users'] = [field[key]] @@ -4048,7 +4050,7 @@ def __init__(self, orig_data, caller, **kwargs): field_info['allow_users'] = {'compute': compile(field[key]['code'], '', 'eval'), 'sourcecode': field[key]['code']} self.find_fields_in(field[key]['code']) else: - raise DAError("An allow users specifier must be a list of integers and plain text items or code." + self.idebug(data)) + raise DASourceError("An allow users specifier must be a list of integers and plain text items or code." + self.idebug(data)) elif key == 'persistent' and 'datatype' in field and field['datatype'] in ('file', 'files', 'camera', 'user', 'environment'): if isinstance(field[key], bool): field_info['persistent'] = field[key] @@ -4086,7 +4088,7 @@ def __init__(self, orig_data, caller, **kwargs): self.find_fields_in(field[key]) elif key in ('js show if', 'js hide if'): if not isinstance(field[key], str): - raise DAError("A js show if or js hide if expression must be a string" + self.idebug(data)) + raise DASourceError("A js show if or js hide if expression must be a string" + self.idebug(data)) js_info = {} if key == 'js show if': js_info['sign'] = True @@ -4100,7 +4102,7 @@ def __init__(self, orig_data, caller, **kwargs): field_info['extras']['show_if_js'] = js_info elif key in ('js disable if', 'js enable if'): if not isinstance(field[key], str): - raise DAError("A js disable if or js enable if expression must be a string" + self.idebug(data)) + raise DASourceError("A js disable if or js enable if expression must be a string" + self.idebug(data)) js_info = {} if key == 'js enable if': js_info['sign'] = True @@ -4119,16 +4121,16 @@ def __init__(self, orig_data, caller, **kwargs): this_is_code = (isinstance(field[key], dict) and len(field[key]) == 1 and 'code' in field[key]) other_is_code = (isinstance(field[other_key], dict) and len(field[other_key]) == 1 and 'code' in field[other_key]) if this_is_code == other_is_code: - raise DAError(key + " cannot be combined with " + other_key) + raise DASourceError(key + " cannot be combined with " + other_key + "." + self.idebug(data)) if 'extras' not in field_info: field_info['extras'] = {} if isinstance(field[key], dict): showif_valid = False if 'variable' in field[key] and 'is' in field[key]: if 'js show if' in field or 'js hide if' in field: - raise DAError("You cannot mix js show if and non-js show if" + self.idebug(data)) + raise DASourceError("You cannot mix js show if and non-js show if" + self.idebug(data)) if 'js disable if' in field or 'js enable if' in field: - raise DAError("You cannot mix js disable if and non-js show if" + self.idebug(data)) + raise DASourceError("You cannot mix js disable if and non-js show if" + self.idebug(data)) field_info['extras']['show_if_var'] = safeid(field[key]['variable'].strip()) if isinstance(field[key]['is'], str): field_info['extras']['show_if_val'] = TextObject(definitions + str(field[key]['is']).strip(), question=self) @@ -4140,14 +4142,14 @@ def __init__(self, orig_data, caller, **kwargs): self.find_fields_in(field[key]['code']) showif_valid = True if not showif_valid: - raise DAError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data)) + raise DASourceError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data)) elif isinstance(field[key], list): - raise DAError("The keys of '" + key + "' cannot be a list" + self.idebug(data)) + raise DASourceError("The keys of '" + key + "' cannot be a list" + self.idebug(data)) elif isinstance(field[key], str): field_info['extras']['show_if_var'] = safeid(field[key].strip()) field_info['extras']['show_if_val'] = TextObject('True') else: - raise DAError("Invalid variable name in show if/hide if") + raise DASourceError("Invalid variable name in show if/hide if." + self.idebug(data)) exclusive = False if isinstance(field[key], dict) and 'code' in field[key]: if len(field[key]) == 1: @@ -4173,16 +4175,16 @@ def __init__(self, orig_data, caller, **kwargs): this_is_code = (isinstance(field[key], dict) and len(field[key]) == 1 and 'code' in field[key]) other_is_code = (isinstance(field[other_key], dict) and len(field[other_key]) == 1 and 'code' in field[other_key]) if this_is_code == other_is_code: - raise DAError(key + " cannot be combined with " + other_key) + raise DASourceError(key + " cannot be combined with " + other_key + "." + self.idebug(data)) if 'extras' not in field_info: field_info['extras'] = {} if isinstance(field[key], dict): showif_valid = False if 'variable' in field[key] and 'is' in field[key]: if 'js show if' in field or 'js hide if' in field: - raise DAError("You cannot mix js show if and non-js disable if" + self.idebug(data)) + raise DASourceError("You cannot mix js show if and non-js disable if." + self.idebug(data)) if 'js disable if' in field or 'js enable if' in field: - raise DAError("You cannot mix js disable if and non-js disable if" + self.idebug(data)) + raise DASourceError("You cannot mix js disable if and non-js disable if." + self.idebug(data)) field_info['extras']['show_if_var'] = safeid(field[key]['variable'].strip()) if isinstance(field[key]['is'], str): field_info['extras']['show_if_val'] = TextObject(definitions + str(field[key]['is']).strip(), question=self) @@ -4194,14 +4196,14 @@ def __init__(self, orig_data, caller, **kwargs): self.find_fields_in(field[key]['code']) showif_valid = True if not showif_valid: - raise DAError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data)) + raise DASourceError("The keys of '" + key + "' must be 'variable' and 'is,' or 'code.'" + self.idebug(data)) elif isinstance(field[key], list): - raise DAError("The keys of '" + key + "' cannot be a list" + self.idebug(data)) + raise DASourceError("The keys of '" + key + "' cannot be a list" + self.idebug(data)) elif isinstance(field[key], str): field_info['extras']['show_if_var'] = safeid(field[key].strip()) field_info['extras']['show_if_val'] = TextObject('True') else: - raise DAError("Invalid variable name in " + key) + raise DASourceError("Invalid variable name in " + key + '.' + self.idebug(data)) exclusive = False if isinstance(field[key], dict) and 'code' in field[key]: if len(field[key]) == 1: @@ -4241,19 +4243,19 @@ def __init__(self, orig_data, caller, **kwargs): auto_determine_type(field_info, the_value=field[key]) elif key == 'disable others': if 'datatype' in field and field['datatype'] in ('file', 'files', 'range', 'multiselect', 'checkboxes', 'camera', 'user', 'environment', 'camcorder', 'microphone', 'object_multiselect', 'object_checkboxes'): # 'yesno', 'yesnowide', 'noyes', 'noyeswide', - raise DAError("A 'disable others' directive cannot be used with this data type." + self.idebug(data)) + raise DASourceError("A 'disable others' directive cannot be used with this data type." + self.idebug(data)) if not isinstance(field[key], (list, bool)): - raise DAError("A 'disable others' directive must be True, False, or a list of variable names." + self.idebug(data)) + raise DASourceError("A 'disable others' directive must be True, False, or a list of variable names." + self.idebug(data)) field_info['disable others'] = field[key] if field[key] is not False: field_info['required'] = False elif key == 'uncheck others' and 'datatype' in field and field['datatype'] in ('yesno', 'yesnowide', 'noyes', 'noyeswide'): if not isinstance(field[key], (list, bool)): - raise DAError("An 'uncheck others' directive must be True, False, or a list of variable names." + self.idebug(data)) + raise DASourceError("An 'uncheck others' directive must be True, False, or a list of variable names." + self.idebug(data)) field_info['uncheck others'] = field[key] elif key == 'check others' and 'datatype' in field and field['datatype'] in ('yesno', 'yesnowide', 'noyes', 'noyeswide'): if not isinstance(field[key], (list, bool)): - raise DAError("A 'check others' directive must be True, False, or a list of variable names." + self.idebug(data)) + raise DASourceError("A 'check others' directive must be True, False, or a list of variable names." + self.idebug(data)) field_info['check others'] = field[key] elif key == 'datatype': field_info['type'] = field[key] @@ -4262,7 +4264,7 @@ def __init__(self, orig_data, caller, **kwargs): if field[key] == 'range' and 'required' not in field_info: field_info['required'] = False if field[key] == 'range' and not ('min' in field and 'max' in field): - raise DAError("If the datatype of a field is 'range', you must provide a min and a max." + self.idebug(data)) + raise DASourceError("If the datatype of a field is 'range', you must provide a min and a max." + self.idebug(data)) if field[key] in ('yesno', 'yesnowide', 'yesnoradio'): field_info['boolean'] = 1 elif field[key] in ('noyes', 'noyeswide', 'noyesradio'): @@ -4278,7 +4280,7 @@ def __init__(self, orig_data, caller, **kwargs): 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'], '', 'eval')] self.find_fields_in(field['exclude']) @@ -4294,7 +4296,7 @@ def __init__(self, orig_data, caller, **kwargs): elif isinstance(field[key], dict): field_info['address_autocomplete'] = field[key] elif isinstance(field[key], list): - raise DAError("address autocomplete must be a Python expression, a dictionary, or a boolean value." + self.idebug(data)) + raise DASourceError("address autocomplete must be a Python expression, a dictionary, or a boolean value." + self.idebug(data)) else: field_info['address_autocomplete'] = bool(field[key]) elif key == 'label above field': @@ -4308,12 +4310,12 @@ def __init__(self, orig_data, caller, **kwargs): if isinstance(field[key], (str, int)): field[key] = {'width': field[key]} if not isinstance(field[key], dict) or len(field[key]) == 0: - raise DAError(key + " is not in the correct format." + self.idebug(data)) + raise DASourceError(key + " is not in the correct format." + self.idebug(data)) for item in field[key].keys(): if item not in ('width', 'label width', 'offset', 'start', 'end', 'breakpoint'): - raise DAError(key + " has an invalid key " + repr(item) + "." + self.idebug(data)) + raise DASourceError(key + " has an invalid key " + repr(item) + "." + self.idebug(data)) if 'width' not in field[key]: - raise DAError(key + ' must specify a width.' + self.idebug(data)) + raise DASourceError(key + ' must specify a width.' + self.idebug(data)) for subkey in ('width', 'label width', 'offset'): if subkey in field[key]: if isinstance(field[key][subkey], str): @@ -4322,7 +4324,7 @@ def __init__(self, orig_data, caller, **kwargs): elif isinstance(field[key][subkey], int): field_info[key][subkey] = field[key][subkey] else: - raise DAError(key + " " + subkey + " must be a number between 1 and 12, or a Python expression." + self.idebug(data)) + raise DASourceError(key + " " + subkey + " must be a number between 1 and 12, or a Python expression." + self.idebug(data)) for subkey in ('start', 'end'): if subkey in field[key]: if isinstance(field[key][subkey], str): @@ -4331,7 +4333,7 @@ def __init__(self, orig_data, caller, **kwargs): elif isinstance(field[key][subkey], (bool, NoneType)): field_info[key][subkey] = field[key][subkey] else: - raise DAError(key + " " + subkey + " must be True or False, or a Python expression." + self.idebug(data)) + raise DASourceError(key + " " + subkey + " must be True or False, or a Python expression." + self.idebug(data)) if 'breakpoint' in field[key]: field_info[key]['breakpoint'] = TextObject(DO_NOT_TRANSLATE + str(field[key]['breakpoint']), question=self) elif key == 'item grid': @@ -4339,19 +4341,19 @@ def __init__(self, orig_data, caller, **kwargs): if isinstance(field[key], (str, int)): field[key] = {'width': field[key]} if not isinstance(field[key], dict) or len(field[key]) == 0: - raise DAError(key + " is not in the correct format." + self.idebug(data)) + raise DASourceError(key + " is not in the correct format." + self.idebug(data)) for item in field[key].keys(): if item not in ('width', 'breakpoint'): - raise DAError(key + " has an invalid key " + repr(item) + "." + self.idebug(data)) + raise DASourceError(key + " has an invalid key " + repr(item) + "." + self.idebug(data)) if 'width' not in field[key]: - raise DAError(key + ' must specify a width.' + self.idebug(data)) + raise DASourceError(key + ' must specify a width.' + self.idebug(data)) if isinstance(field[key]['width'], str): field_info[key]['width'] = compile(field[key]['width'], '<' + key + ' width expression>', 'eval') self.find_fields_in(field[key]['width']) elif isinstance(field[key]['width'], int): field_info[key]['width'] = field[key]['width'] else: - raise DAError(key + " width must be a number between 1 and 12, or a Python expression." + self.idebug(data)) + raise DASourceError(key + " width must be a number between 1 and 12, or a Python expression." + self.idebug(data)) if 'breakpoint' in field[key]: field_info[key]['breakpoint'] = TextObject(DO_NOT_TRANSLATE + str(field[key]['breakpoint']), question=self) elif key == 'floating label': @@ -4362,13 +4364,13 @@ def __init__(self, orig_data, caller, **kwargs): field_info['floating_label'] = bool(field[key]) elif key == 'action' and 'input type' in field and field['input type'] == 'ajax': if not isinstance(field[key], str): - raise DAError("An action must be plain text" + self.idebug(data)) + raise DASourceError("An action must be plain text" + self.idebug(data)) if 'combobox action' not in field_info: field_info['combobox action'] = {'trig': 4} field_info['combobox action']['action'] = field[key] elif key == 'trigger at' and 'action' in field and 'input type' in field and field['input type'] == 'ajax': if (not isinstance(field[key], int)) or field[key] < 2: - raise DAError("A trigger at must an integer greater than one" + self.idebug(data)) + raise DASourceError("A trigger at must an integer greater than one" + self.idebug(data)) if 'combobox action' not in field_info: field_info['combobox action'] = {} field_info['combobox action']['trig'] = field[key] @@ -4378,7 +4380,7 @@ def __init__(self, orig_data, caller, **kwargs): if 'datatype' in field and field['datatype'] in ('object', 'object_radio', 'object_multiselect', 'object_checkboxes'): field_info['choicetype'] = 'compute' if not isinstance(field[key], (list, str)): - raise DAError("choices is not in appropriate format" + self.idebug(data)) + raise DASourceError("choices is not in appropriate format" + self.idebug(data)) field_info['selections'] = {} else: field_info['choicetype'] = 'manual' @@ -4393,7 +4395,7 @@ def __init__(self, orig_data, caller, **kwargs): 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(), '', 'eval')] @@ -4436,26 +4438,26 @@ def __init__(self, orig_data, caller, **kwargs): field_info['nota'] = TextObject(definitions + interpret_label(field[key]), question=self) elif key == 'field': if 'label' not in field: - raise DAError("If you use 'field' to indicate a variable in a 'fields' section, you must also include a 'label.'" + self.idebug(data)) + raise DASourceError("If you use 'field' to indicate a variable in a 'fields' section, you must also include a 'label.'" + self.idebug(data)) if not isinstance(field[key], str): - raise DAError("Fields in a 'field' section must be plain text." + self.idebug(data)) + raise DASourceError("Fields in a 'field' section must be plain text." + self.idebug(data)) field[key] = field[key].strip() if invalid_variable_name(field[key]): - raise DAError("Missing or invalid variable name " + repr(field[key]) + "." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(field[key]) + "." + self.idebug(data)) field_info['saveas'] = field[key] elif key == 'label': if 'field' not in field: - raise DAError("If you use 'label' to label a field in a 'fields' section, you must also include a 'field.'" + self.idebug(data)) + raise DASourceError("If you use 'label' to label a field in a 'fields' section, you must also include a 'field.'" + self.idebug(data)) field_info['label'] = TextObject(definitions + interpret_label(field[key]), question=self) else: if 'label' in field_info: - raise DAError("Syntax error: field label '" + str(key) + "' overwrites previous label, '" + str(field_info['label'].original_text) + "'" + self.idebug(data)) + raise DASourceError("Syntax error: field label '" + str(key) + "' overwrites previous label, '" + str(field_info['label'].original_text) + "'" + self.idebug(data)) field_info['label'] = TextObject(definitions + interpret_label(key), question=self) if not isinstance(field[key], str): - raise DAError("Fields in a 'field' section must be plain text." + self.idebug(data)) + raise DASourceError("Fields in a 'field' section must be plain text." + self.idebug(data)) field[key] = field[key].strip() if invalid_variable_name(field[key]): - raise DAError("Missing or invalid variable name " + repr(field[key]) + " for key " + repr(key) + "." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(field[key]) + " for key " + repr(key) + "." + self.idebug(data)) field_info['saveas'] = field[key] if 'type' in field_info: if field_info['type'] in ('multiselect', 'object_multiselect', 'checkboxes', 'object_checkboxes'): @@ -4467,14 +4469,14 @@ def __init__(self, orig_data, caller, **kwargs): field_info['nota'] = False if 'choicetype' in field_info and field_info['choicetype'] == 'compute' and 'type' in field_info and field_info['type'] in ('object', 'object_radio', 'object_multiselect', 'object_checkboxes'): if 'choices' not in field: - raise DAError("You need to have a choices element if you want to set a variable to an object." + self.idebug(data)) + raise DASourceError("You need to have a choices element if you want to set a variable to an object." + self.idebug(data)) if not isinstance(field['choices'], list): select_list = [str(field['choices'])] else: select_list = field['choices'] if 'exclude' in field: if isinstance(field['exclude'], dict): - raise DAError("choices exclude list is not in appropriate format" + self.idebug(data)) + raise DASourceError("choices exclude list is not in appropriate format" + self.idebug(data)) if not isinstance(field['exclude'], list): exclude_list = [str(field['exclude']).strip()] else: @@ -4483,7 +4485,7 @@ def __init__(self, orig_data, caller, **kwargs): select_list.append('exclude=[' + ", ".join(exclude_list) + ']') if 'default' in field: if not isinstance(field['default'], (list, str)): - raise DAError("default list is not in appropriate format" + self.idebug(data)) + raise DASourceError("default list is not in appropriate format" + self.idebug(data)) if not isinstance(field['default'], list): default_list = [str(field['default'])] else: @@ -4506,7 +4508,7 @@ def __init__(self, orig_data, caller, **kwargs): field_info['selections'] = {'compute': compile(source_code, '', 'eval'), 'sourcecode': source_code} if 'saveas' in field_info: if not isinstance(field_info['saveas'], str): - raise DAError("Invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data)) + raise DASourceError("Invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data)) self.fields.append(Field(field_info)) if 'type' in field_info: if field_info['type'] in ('multiselect', 'object_multiselect', 'checkboxes', 'object_checkboxes'): @@ -4533,7 +4535,7 @@ def __init__(self, orig_data, caller, **kwargs): if re.search(r'\.text$', field_info['saveas']): field_info['saveas'] = field_info['saveas'].strip() if invalid_variable_name(field_info['saveas']): - raise DAError("Missing or invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(field_info['saveas']) + "." + self.idebug(data)) field_info['saveas'] = re.sub(r'\.text$', '', field_info['saveas']) if self.scan_for_variables: self.fields_used.add(field_info['saveas']) @@ -4554,7 +4556,7 @@ def __init__(self, orig_data, caller, **kwargs): self.fields_used.add(field_info['saveas']) else: self.other_fields_used.add(field_info['saveas']) - elif 'note' in field or 'html' or 'raw html' in field: + elif 'note' in field or 'html' in field or 'raw html' in field: if 'note' in field: field_info['type'] = 'note' elif 'raw html' in field: @@ -4563,14 +4565,14 @@ def __init__(self, orig_data, caller, **kwargs): field_info['type'] = 'html' self.fields.append(Field(field_info)) else: - raise DAError("A field was listed without indicating a label or a variable name, and the field was not a note or raw HTML." + self.idebug(data) + " and field_info was " + repr(field_info)) + raise DASourceError("A field was listed without indicating a label or a variable name, and the field was not a note or raw HTML." + self.idebug(data) + " and field_info was " + repr(field_info)) field_number += 1 if 'current_field' in docassemble.base.functions.this_thread.misc: del docassemble.base.functions.this_thread.misc['current_field'] if 'review' in data: self.question_type = 'review' if self.is_mandatory and 'continue button field' not in data: - raise DAError("A review block without a continue button field cannot be mandatory." + self.idebug(data)) + raise DASourceError("A review block without a continue button field cannot be mandatory." + self.idebug(data)) if 'tabular' in data and data['tabular']: if isinstance(data['tabular'], str): tabular_class = data['tabular'] @@ -4580,11 +4582,11 @@ def __init__(self, orig_data, caller, **kwargs): if isinstance(data['review'], dict): data['review'] = [data['review']] if not isinstance(data['review'], list): - raise DAError("The review must be written in the form of a list." + self.idebug(data)) + raise DASourceError("The review must be written in the form of a list." + self.idebug(data)) field_number = 0 for field in data['review']: if not isinstance(field, dict): - raise DAError("Each individual field in a list of fields must be expressed as a dictionary item, e.g., ' - Fruit: user.favorite_fruit'." + self.idebug(data)) + raise DASourceError("Each individual field in a list of fields must be expressed as a dictionary item, e.g., ' - Fruit: user.favorite_fruit'." + self.idebug(data)) field_info = {'number': field_number, 'data': []} for key in field: if key == 'action': @@ -4593,7 +4595,7 @@ def __init__(self, orig_data, caller, **kwargs): if not isinstance(field[key], dict) and not isinstance(field[key], list): field_info[key] = TextObject(definitions + str(field[key]), question=self) if 'button' in field: # or 'css' in field or 'script' in field: - raise DAError("In a review block, you cannot mix help text with a button item." + self.idebug(data)) # , css, or script + raise DASourceError("In a review block, you cannot mix help text with a button item." + self.idebug(data)) # , css, or script elif key == 'button': if not isinstance(field[key], dict) and not isinstance(field[key], list): field_info['help'] = TextObject(definitions + str(field[key]), question=self) @@ -4612,10 +4614,10 @@ def __init__(self, orig_data, caller, **kwargs): field_data = [] for the_saveas in field_list: # if not isinstance(the_saveas, str): - # raise DAError("Invalid variable name in fields." + self.idebug(data)) + # raise DASourceError("Invalid variable name in fields." + self.idebug(data)) the_saveas = str(the_saveas).strip() # if invalid_variable_name(the_saveas): - # raise DAError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data)) + # raise DASourceError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data)) if the_saveas not in field_data: field_data.append(the_saveas) self.find_fields_in(the_saveas) @@ -4625,7 +4627,7 @@ def __init__(self, orig_data, caller, **kwargs): field_info['saveas_code'].extend([(compile(y, '', 'eval'), True) for y in field_list]) elif key in ('field', 'fields'): if 'label' not in field: - raise DAError("If you use 'field' or 'fields' to indicate variables in a 'review' section, you must also include a 'label.'" + self.idebug(data)) + raise DASourceError("If you use 'field' or 'fields' to indicate variables in a 'review' section, you must also include a 'label.'" + self.idebug(data)) if not isinstance(field[key], list): field_list = [field[key]] else: @@ -4635,29 +4637,29 @@ def __init__(self, orig_data, caller, **kwargs): if isinstance(the_saveas, dict) and len(the_saveas) == 1 and ('undefine' in the_saveas or 'invalidate' in the_saveas or 'recompute' in the_saveas or 'set' in the_saveas or 'follow up' in the_saveas): if 'set' in the_saveas: if not isinstance(the_saveas['set'], list): - raise DAError("The set statement must refer to a list." + self.idebug(data)) + raise DASourceError("The set statement must refer to a list." + self.idebug(data)) clean_list = [] for the_dict in the_saveas['set']: if not isinstance(the_dict, dict): - raise DAError("A set command must refer to a list of dicts." + self.idebug(data)) + raise DASourceError("A set command must refer to a list of dicts." + self.idebug(data)) for the_var, the_val in the_dict.items(): if not isinstance(the_var, str): - raise DAError("A set command must refer to a list of dicts with keys as variable names." + self.idebug(data)) + raise DASourceError("A set command must refer to a list of dicts with keys as variable names." + self.idebug(data)) the_var_stripped = the_var.strip() if invalid_variable_name(the_var_stripped): - raise DAError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data)) self.find_fields_in(the_var_stripped) clean_list.append([the_var_stripped, the_val]) field_info['data'].append({'action': '_da_set', 'arguments': {'variables': clean_list}}) if 'follow up' in the_saveas: if not isinstance(the_saveas['follow up'], list): - raise DAError("The follow up statement must refer to a list." + self.idebug(data)) + raise DASourceError("The follow up statement must refer to a list." + self.idebug(data)) for var in the_saveas['follow up']: if not isinstance(var, str): - raise DAError("Invalid variable name in follow up " + repr(var) + "." + self.idebug(data)) + raise DASourceError("Invalid variable name in follow up " + repr(var) + "." + self.idebug(data)) var_saveas = var.strip() if invalid_variable_name(var_saveas): - raise DAError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data)) self.find_fields_in(var_saveas) # field_info['data'].append({'action': "_da_follow_up", 'arguments': {'action': var}}) field_info['data'].append({'action': var, 'arguments': {}}) @@ -4665,14 +4667,14 @@ def __init__(self, orig_data, caller, **kwargs): if command not in the_saveas: continue if not isinstance(the_saveas[command], list): - raise DAError("The " + command + " statement must refer to a list." + self.idebug(data)) + raise DASourceError("The " + command + " statement must refer to a list." + self.idebug(data)) clean_list = [] for undef_var in the_saveas[command]: if not isinstance(undef_var, str): - raise DAError("Invalid variable name " + repr(undef_var) + " in " + command + "." + self.idebug(data)) + raise DASourceError("Invalid variable name " + repr(undef_var) + " in " + command + "." + self.idebug(data)) undef_saveas = undef_var.strip() if invalid_variable_name(undef_saveas): - raise DAError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data)) self.find_fields_in(undef_saveas) clean_list.append(undef_saveas) if command == 'invalidate': @@ -4684,13 +4686,13 @@ def __init__(self, orig_data, caller, **kwargs): continue if isinstance(the_saveas, dict) and len(the_saveas) == 2 and 'action' in the_saveas and 'arguments' in the_saveas: if not isinstance(the_saveas['arguments'], dict): - raise DAError("An arguments directive must refer to a dictionary. " + repr(data)) + raise DASourceError("An arguments directive must refer to a dictionary. " + repr(data)) field_info['data'].append({'action': the_saveas['action'], 'arguments': the_saveas['arguments']}) if not isinstance(the_saveas, str): - raise DAError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data)) + raise DASourceError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data)) the_saveas = the_saveas.strip() if invalid_variable_name(the_saveas): - raise DAError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data)) if the_saveas not in field_info['data']: field_info['data'].append(the_saveas) self.find_fields_in(the_saveas) @@ -4698,7 +4700,7 @@ def __init__(self, orig_data, caller, **kwargs): field_info['action'] = {'action': field['action'], 'arguments': {}} elif key == 'label': if 'field' not in field and 'fields' not in field: - raise DAError("If you use 'label' to label a field in a 'review' section, you must also include a 'field' or 'fields.'" + self.idebug(data)) + raise DASourceError("If you use 'label' to label a field in a 'review' section, you must also include a 'field' or 'fields.'" + self.idebug(data)) field_info['label'] = TextObject(definitions + interpret_label(field[key]), question=self) else: field_info['label'] = TextObject(definitions + interpret_label(key), question=self) @@ -4711,29 +4713,29 @@ def __init__(self, orig_data, caller, **kwargs): if isinstance(the_saveas, dict) and len(the_saveas) == 1 and ('undefine' in the_saveas or 'invalidate' in the_saveas or 'recompute' in the_saveas or 'set' in the_saveas or 'follow up' in the_saveas): if 'set' in the_saveas: if not isinstance(the_saveas['set'], list): - raise DAError("The set statement must refer to a list." + self.idebug(data)) + raise DASourceError("The set statement must refer to a list." + self.idebug(data)) clean_list = [] for the_dict in the_saveas['set']: if not isinstance(the_dict, dict): - raise DAError("A set command must refer to a list of dicts." + self.idebug(data)) + raise DASourceError("A set command must refer to a list of dicts." + self.idebug(data)) for the_var, the_val in the_dict.items(): if not isinstance(the_var, str): - raise DAError("A set command must refer to a list of dicts with keys as variable names." + self.idebug(data)) + raise DASourceError("A set command must refer to a list of dicts with keys as variable names." + self.idebug(data)) the_var_stripped = the_var.strip() if invalid_variable_name(the_var_stripped): - raise DAError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(the_var) + " ." + self.idebug(data)) self.find_fields_in(the_var_stripped) clean_list.append([the_var_stripped, the_val]) field_info['data'].append({'action': '_da_set', 'arguments': {'variables': clean_list}}) if 'follow up' in the_saveas: if not isinstance(the_saveas['follow up'], list): - raise DAError("The follow up statement must refer to a list." + self.idebug(data)) + raise DASourceError("The follow up statement must refer to a list." + self.idebug(data)) for var in the_saveas['follow up']: if not isinstance(var, str): - raise DAError("Invalid variable name in follow up " + command + "." + self.idebug(data)) + raise DASourceError("Invalid variable name in follow up " + command + "." + self.idebug(data)) var_saveas = var.strip() if invalid_variable_name(var_saveas): - raise DAError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(var_saveas) + " ." + self.idebug(data)) self.find_fields_in(var_saveas) # field_info['data'].append({'action': "_da_follow_up", 'arguments': {'action': var}}) field_info['data'].append({'action': var, 'arguments': {}}) @@ -4741,14 +4743,14 @@ def __init__(self, orig_data, caller, **kwargs): if command not in the_saveas: continue if not isinstance(the_saveas[command], list): - raise DAError("The " + command + " statement must refer to a list." + self.idebug(data)) + raise DASourceError("The " + command + " statement must refer to a list." + self.idebug(data)) clean_list = [] for undef_var in the_saveas[command]: if not isinstance(undef_var, str): - raise DAError("Invalid variable name " + repr(undef_var) + " in fields " + command + "." + self.idebug(data)) + raise DASourceError("Invalid variable name " + repr(undef_var) + " in fields " + command + "." + self.idebug(data)) undef_saveas = undef_var.strip() if invalid_variable_name(undef_saveas): - raise DAError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(undef_saveas) + " ." + self.idebug(data)) self.find_fields_in(undef_saveas) clean_list.append(undef_saveas) if command == 'invalidate': @@ -4760,13 +4762,13 @@ def __init__(self, orig_data, caller, **kwargs): continue if isinstance(the_saveas, dict) and len(the_saveas) == 2 and 'action' in the_saveas and 'arguments' in the_saveas: if not isinstance(the_saveas['arguments'], dict): - raise DAError("An arguments directive must refer to a dictionary. " + repr(data)) + raise DASourceError("An arguments directive must refer to a dictionary. " + repr(data)) field_info['data'].append({'action': the_saveas['action'], 'arguments': the_saveas['arguments']}) if not isinstance(the_saveas, str): - raise DAError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data)) + raise DASourceError("Invalid variable name " + repr(the_saveas) + " in fields." + self.idebug(data)) the_saveas = the_saveas.strip() if invalid_variable_name(the_saveas): - raise DAError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data)) + raise DASourceError("Missing or invalid variable name " + repr(the_saveas) + " ." + self.idebug(data)) # if the_saveas not in field_info['data']: field_info['data'].append(the_saveas) self.find_fields_in(the_saveas) @@ -4786,7 +4788,7 @@ def __init__(self, orig_data, caller, **kwargs): if len(field_info['data']) > 0 or ('type' in field_info and field_info['type'] in ('note', 'html', 'raw html')): self.fields.append(Field(field_info)) else: - raise DAError("A field in a review list was listed without indicating a label or a variable name, and the field was not a note or raw HTML." + self.idebug(field_info)) + raise DASourceError("A field in a review list was listed without indicating a label or a variable name, and the field was not a note or raw HTML." + self.idebug(field_info)) field_number += 1 if not hasattr(self, 'question_type'): if len(self.attachments) > 0 and len(self.fields_used) > 0 and not hasattr(self, 'content'): @@ -4795,7 +4797,7 @@ def __init__(self, orig_data, caller, **kwargs): self.question_type = 'deadend' if should_append: if not hasattr(self, 'question_type'): - raise DAError("No question type could be determined for this section." + self.idebug(data)) + raise DASourceError("No question type could be determined for this section." + self.idebug(data)) if main_list: self.interview.questions_list.append(self) self.number = self.interview.next_number() @@ -4803,7 +4805,7 @@ def __init__(self, orig_data, caller, **kwargs): if hasattr(self, 'id'): self.name = "ID " + self.id # if self.name in self.interview.questions_by_name: - # raise DAError("Question ID " + str(self.id) + " results in duplicate question name") + # raise DASourceError("Question ID " + str(self.id) + " results in duplicate question name") else: self.name = "Question_" + str(self.number) else: @@ -5055,7 +5057,7 @@ def process_attachment(self, orig_target): options['skip_undefined'] = compile(target['skip undefined'], '', 'eval') self.find_fields_in(target['skip undefined']) else: - raise DAError('Unknown data type in attachment skip undefined.' + self.idebug(target)) + raise DASourceError('Unknown data type in attachment skip undefined.' + self.idebug(target)) else: options['skip_undefined'] = False if 'language' in target: @@ -5075,11 +5077,11 @@ def process_attachment(self, orig_target): self.find_fields_in(target['redact']) if 'checkbox export value' in target and 'pdf template file' in target: if not isinstance(target['checkbox export value'], str): - raise DAError("A checkbox export value must be a string." + self.idebug(target)) + raise DASourceError("A checkbox export value must be a string." + self.idebug(target)) options['checkbox_export_value'] = TextObject(target['checkbox export value']) if 'decimal places' in target and 'pdf template file' in target: if not isinstance(target['decimal places'], (str, int)): - raise DAError("A decimal places directive must be an integer or string." + self.idebug(target)) + raise DASourceError("A decimal places directive must be an integer or string." + self.idebug(target)) options['decimal_places'] = TextObject(str(target['decimal places'])) if 'initial yaml' in target: if not isinstance(target['initial yaml'], list): @@ -5087,7 +5089,7 @@ def process_attachment(self, orig_target): options['initial_yaml'] = [] for yaml_file in target['initial yaml']: if not isinstance(yaml_file, str): - raise DAError('An initial yaml file must be a string.' + self.idebug(target)) + raise DASourceError('An initial yaml file must be a string.' + self.idebug(target)) options['initial_yaml'].append(FileInPackage(yaml_file, 'template', self.package)) if 'additional yaml' in target: if not isinstance(target['additional yaml'], list): @@ -5095,19 +5097,19 @@ def process_attachment(self, orig_target): options['additional_yaml'] = [] for yaml_file in target['additional yaml']: if not isinstance(yaml_file, str): - raise DAError('An additional yaml file must be a string.' + self.idebug(target)) + raise DASourceError('An additional yaml file must be a string.' + self.idebug(target)) options['additional_yaml'].append(FileInPackage(yaml_file, 'template', self.package)) if 'template file' in target: if not isinstance(target['template file'], str): - raise DAError('The template file must be a string.' + self.idebug(target)) + raise DASourceError('The template file must be a string.' + self.idebug(target)) options['template_file'] = FileInPackage(target['template file'], 'template', self.package) if 'rtf template file' in target: if not isinstance(target['rtf template file'], str): - raise DAError('The rtf template file must be a string.' + self.idebug(target)) + raise DASourceError('The rtf template file must be a string.' + self.idebug(target)) options['rtf_template_file'] = FileInPackage(target['rtf template file'], 'template', self.package) if 'docx reference file' in target: if not isinstance(target['docx reference file'], str): - raise DAError('The docx reference file must be a string.' + self.idebug(target)) + raise DASourceError('The docx reference file must be a string.' + self.idebug(target)) options['docx_reference_file'] = FileInPackage(target['docx reference file'], 'template', self.package) if 'usedefs' in target: if isinstance(target['usedefs'], str): @@ -5115,17 +5117,17 @@ def process_attachment(self, orig_target): elif isinstance(target['usedefs'], list): the_list = target['usedefs'] else: - raise DAError('The usedefs included in an attachment must be specified as a list of strings or a single string.' + self.idebug(target)) + raise DASourceError('The usedefs included in an attachment must be specified as a list of strings or a single string.' + self.idebug(target)) for def_key in the_list: if not isinstance(def_key, str): - raise DAError('The defs in an attachment must be strings.' + self.idebug(target)) + raise DASourceError('The defs in an attachment must be strings.' + self.idebug(target)) if def_key not in self.interview.defs: - raise DAError('Referred to a non-existent def "' + def_key + '." All defs must be defined before they are used.' + self.idebug(target)) + raise DASourceError('Referred to a non-existent def "' + def_key + '." All defs must be defined before they are used.' + self.idebug(target)) defs.extend(self.interview.defs[def_key]) if 'variable name' in target: variable_name = target['variable name'] if variable_name is None: - raise DAError('A variable name cannot be None.' + self.idebug(target)) + raise DASourceError('A variable name cannot be None.' + self.idebug(target)) if self.scan_for_variables: self.fields_used.add(target['variable name']) else: @@ -5134,13 +5136,13 @@ def process_attachment(self, orig_target): variable_name = "_internal['docvar'][" + str(self.interview.next_attachment_number()) + "]" if 'metadata' in target: if not isinstance(target['metadata'], dict): - raise DAError('Unknown data type ' + str(type(target['metadata'])) + ' in attachment metadata.' + self.idebug(target)) + raise DASourceError('Unknown data type ' + str(type(target['metadata'])) + ' in attachment metadata.' + self.idebug(target)) for key in target['metadata']: data = target['metadata'][key] if isinstance(data, list): for sub_data in data: if not isinstance(sub_data, str): - raise DAError('Unknown data type ' + str(type(sub_data)) + ' in list in attachment metadata' + self.idebug(target)) + raise DASourceError('Unknown data type ' + str(type(sub_data)) + ' in list in attachment metadata' + self.idebug(target)) newdata = list(map((lambda x: TextObject(x, question=self)), data)) metadata[key] = newdata elif isinstance(data, str): @@ -5148,7 +5150,7 @@ def process_attachment(self, orig_target): elif isinstance(data, bool): metadata[key] = data else: - raise DAError('Unknown data type ' + str(type(data)) + ' in key in attachment metadata' + self.idebug(target)) + raise DASourceError('Unknown data type ' + str(type(data)) + ' in key in attachment metadata' + self.idebug(target)) if 'raw' in target and target['raw']: if 'content file' in target: content_file = target['content file'] @@ -5176,20 +5178,20 @@ def process_attachment(self, orig_target): options['content file code'] = compile(target['content file']['code'], '', 'eval') self.find_fields_in(target['content file']['code']) else: - raise DAError('A content file must be specified as text, a list of text filenames, or a dictionary where the one key is code' + self.idebug(target)) + raise DASourceError('A content file must be specified as text, a list of text filenames, or a dictionary where the one key is code' + self.idebug(target)) else: if not isinstance(target['content file'], list): target['content file'] = [target['content file']] target['content'] = '' for content_file in target['content file']: if not isinstance(content_file, str): - raise DAError('A content file must be specified as text, a list of text filenames, or a dictionary where the one key is code' + self.idebug(target)) + raise DASourceError('A content file must be specified as text, a list of text filenames, or a dictionary where the one key is code' + self.idebug(target)) file_to_read = docassemble.base.functions.package_template_filename(content_file, package=self.package) if file_to_read is not None and os.path.isfile(file_to_read) and os.access(file_to_read, os.R_OK): with open(file_to_read, 'r', encoding='utf-8') as the_file: target['content'] += the_file.read() else: - raise DAError('Unable to read content file ' + str(content_file) + ' after trying to find it at ' + str(file_to_read) + self.idebug(target)) + raise DASourceError('Unable to read content file ' + str(content_file) + ' after trying to find it at ' + str(file_to_read) + self.idebug(target)) if 'pdf template file' in target and ('code' in target or 'field variables' in target or 'field code' in target or 'raw field variables' in target) and 'fields' not in target: target['fields'] = {} field_mode = 'manual' @@ -5201,7 +5203,7 @@ def process_attachment(self, orig_target): options['update_references'] = compile(target['update references'], '', 'eval') self.find_fields_in(target['update references']) else: - raise DAError('Unknown data type in attachment "update references".' + self.idebug(target)) + raise DASourceError('Unknown data type in attachment "update references".' + self.idebug(target)) if 'fields' in target: field_mode = 'manual' else: @@ -5216,9 +5218,9 @@ def process_attachment(self, orig_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'] @@ -5230,37 +5232,37 @@ def process_attachment(self, orig_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: @@ -5322,22 +5324,22 @@ def process_attachment(self, orig_target): 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, '', '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, '', 'eval') self.find_fields_in(varname) if 'field code' in target: @@ -5347,7 +5349,7 @@ def process_attachment(self, orig_target): 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), '', 'eval') self.find_fields_in(val) @@ -5355,9 +5357,9 @@ def process_attachment(self, orig_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: @@ -5373,7 +5375,7 @@ def process_attachment(self, orig_target): options['persistent'] = compile(target['persistent'], '', 'eval') self.find_fields_in(target['persistent']) else: - raise DAError('Unknown data type in attachment persistent.' + self.idebug(target)) + raise DASourceError('Unknown data type in attachment persistent.' + self.idebug(target)) if 'private' in target: if isinstance(target['private'], bool): options['private'] = target['private'] @@ -5381,7 +5383,7 @@ def process_attachment(self, orig_target): options['private'] = compile(target['private'], '', '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'], '', 'eval') @@ -5390,7 +5392,7 @@ def process_attachment(self, orig_target): elif isinstance(target['allow privileges'], list): for item in target['allow privileges']: if not isinstance(item, str): - raise DAError('Unknown data type in attachment allow privileges.' + self.idebug(target)) + raise DASourceError('Unknown data type in attachment allow privileges.' + self.idebug(target)) options['allow privileges'] = target['allow privileges'] if 'allow users' in target: if isinstance(target['allow users'], dict) and len(target['allow users']) == 1 and 'code' in target['allow users'] and isinstance(target['allow users']['code'], str): @@ -5400,13 +5402,13 @@ def process_attachment(self, orig_target): elif isinstance(target['allow users'], list): for item in target['allow users']: if not isinstance(item, (str, int)): - raise DAError('Unknown data type in attachment allow users.' + self.idebug(target)) + raise DASourceError('Unknown data type in attachment allow users.' + self.idebug(target)) options['allow users'] = target['allow users'] if 'hyperlink style' in target: if isinstance(target['hyperlink style'], str): options['hyperlink_style'] = TextObject(target['hyperlink style'].strip(), question=self) else: - raise DAError('Unknown data type in attachment hyperlink style.' + self.idebug(target)) + raise DASourceError('Unknown data type in attachment hyperlink style.' + self.idebug(target)) if 'pdf/a' in target: if isinstance(target['pdf/a'], bool): options['pdf_a'] = target['pdf/a'] @@ -5414,7 +5416,15 @@ def process_attachment(self, orig_target): options['pdf_a'] = compile(target['pdf/a'], '', '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'], '', 'eval') + self.find_fields_in(target['pdftk']) + else: + raise DASourceError('Unknown data type in attachment pdftk.' + self.idebug(target)) if 'rendering font' in target and target['rendering font']: options['rendering_font'] = TextObject(str(target['rendering font']), question=self) if 'tagged pdf' in target: @@ -5424,11 +5434,11 @@ def process_attachment(self, orig_target): options['tagged_pdf'] = compile(target['tagged pdf'], '', 'eval') self.find_fields_in(target['tagged pdf']) else: - raise DAError('Unknown data type in attachment tagged pdf.' + self.idebug(target)) + raise DASourceError('Unknown data type in attachment tagged pdf.' + self.idebug(target)) if 'content' not in target: if 'content file code' in options: return {'name': TextObject(target['name'], question=self), 'filename': TextObject(target['filename'], question=self), 'description': TextObject(target['description'], question=self), 'content': None, 'valid_formats': target['valid formats'], 'metadata': metadata, 'variable_name': variable_name, 'orig_variable_name': variable_name, 'options': options, 'raw': target['raw']} - raise DAError("No content provided in attachment." + self.idebug(target)) + raise DASourceError("No content provided in attachment." + self.idebug(target)) # logmessage("The content is " + str(target['content'])) return {'name': TextObject(target['name'], question=self), 'filename': TextObject(target['filename'], question=self), 'description': TextObject(target['description'], question=self), 'content': TextObject("\n".join(defs) + "\n" + target['content'], question=self), 'valid_formats': target['valid formats'], 'metadata': metadata, 'variable_name': variable_name, 'orig_variable_name': variable_name, 'options': options, 'raw': target['raw']} if isinstance(orig_target, str): @@ -6111,7 +6121,10 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p extra_amount = get_config('list collect extra count', 15) for list_indexno in range(length_to_use + extra_amount): new_iterators = copy.copy(iterators) - new_iterators[iterator_index] = str(list_indexno) + try: + new_iterators[iterator_index] = str(list_indexno) + except IndexError: + raise DAException("list collect question needs iterator " + extras['list_iterator'] + " but it was asked in a context where there is no " + extras['list_iterator']) ask_result = self.ask(user_dict, old_user_dict, the_x, new_iterators, sought, orig_sought, process_list_collect=False, test_for_objects=(list_indexno < length_to_use)) if hasattr(self, 'list_collect_label'): extras['list_message'][list_indexno] = self.list_collect_label.text(user_dict) @@ -6358,7 +6371,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p if hasattr(field, 'saveas'): parse_result = parse_var_name(from_safeid(field.saveas)) if not parse_result['valid']: - raise DAError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason']) + raise DASourceError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason']) if len(parse_result['objects']) > 0: assumed_objects.add(parse_result['objects'][-1]) if len(parse_result['bracket_objects']) > 0: @@ -6539,7 +6552,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p # logmessage("Doing " + the_string) exec(the_string, user_dict) except Exception as err: - raise DAError("Failure while processing field with datatype of object: " + err.__class__.__name__ + " " + str(err)) + raise DASourceError("Failure while processing field with datatype of object: " + err.__class__.__name__ + " " + str(err)) if hasattr(field, 'label'): labels[field.number] = field.label.text(user_dict) if hasattr(field, 'extras'): @@ -6691,7 +6704,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p # assumed_objects.add(m.group(1)) parse_result = parse_var_name(from_safeid(field.saveas)) if not parse_result['valid']: - raise DAError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason']) + raise DASourceError("Variable name " + from_safeid(field.saveas) + " is invalid: " + parse_result['reason']) if len(parse_result['objects']) > 0: assumed_objects.add(parse_result['objects'][-1]) if len(parse_result['bracket_objects']) > 0: @@ -6794,12 +6807,12 @@ def parse_fields(self, the_list, register_target, uses_field): new_list.append(new_item) the_list = new_list if not isinstance(the_list, list): - raise DAError("Multiple choices need to be provided in list form. " + self.idebug(the_list)) + raise DASourceError("Multiple choices need to be provided in list form. " + self.idebug(the_list)) for the_dict in the_list: if not isinstance(the_dict, (dict, list)): the_dict = {str(the_dict): the_dict} elif not isinstance(the_dict, dict): - raise DAError("Unknown data type for the_dict in parse_fields. " + self.idebug(the_list)) + raise DASourceError("Unknown data type for the_dict in parse_fields. " + self.idebug(the_list)) result_dict = {} uses_value_label = 'value' in the_dict and 'label' in the_dict for key, value in the_dict.items(): @@ -6850,7 +6863,7 @@ def parse_fields(self, the_list, register_target, uses_field): result_dict['label'] = TextObject(key, question=self) result_dict['key'] = value else: - raise DAError("Unknown data type in parse_fields:" + str(type(value)) + ". " + self.idebug(the_list)) + raise DASourceError("Unknown data type in parse_fields:" + str(type(value)) + ". " + self.idebug(the_list)) result_list.append(result_dict) return (has_code, result_list) @@ -6927,7 +6940,7 @@ def finalize_attachment(self, attachment, result, the_user_dict): if hasattr(the_file, 'number'): result['file'][doc_format] = the_file.number # logmessage("finalize_attachment: returning " + attachment['variable_name'] + " from cache") - for key in ('template', 'field_data', 'images', 'data_strings', 'convert_to_pdf_a', 'convert_to_tagged_pdf', 'password', 'owner_password', 'template_password', 'update_references', 'permissions', 'rendering_font'): + for key in ('template', 'field_data', 'images', 'data_strings', 'convert_to_pdf_a', 'use_pdftk', 'convert_to_tagged_pdf', 'password', 'owner_password', 'template_password', 'update_references', 'permissions', 'rendering_font'): if key in result: del result[key] return result @@ -6965,10 +6978,10 @@ def finalize_attachment(self, attachment, result, the_user_dict): docassemble.base.functions.set_context('pdf') the_template_path = attachment['options']['pdf_template_file'].path(the_user_dict=the_user_dict) if the_template_path is None: - raise DAError("pdf template file " + attachment['options']['pdf_template_file'].original_reference() + " not found") - the_pdf_file = docassemble.base.pdftk.fill_template(the_template_path, data_strings=result['data_strings'], images=result['images'], editable=result['editable'], pdfa=result['convert_to_pdf_a'], password=result['password'], owner_password=result['owner_password'], template_password=result['template_password'], default_export_value=default_export_value, replacement_font=result['rendering_font']) + raise DASourceError("pdf template file " + attachment['options']['pdf_template_file'].original_reference() + " not found") + the_pdf_file = docassemble.base.pdftk.fill_template(the_template_path, data_strings=result['data_strings'], images=result['images'], editable=result['editable'], pdfa=result['convert_to_pdf_a'], use_pdftk=result['use_pdftk'], password=result['password'], owner_password=result['owner_password'], template_password=result['template_password'], default_export_value=default_export_value, replacement_font=result['rendering_font']) result['file'][doc_format], result['extension'][doc_format], result['mimetype'][doc_format] = docassemble.base.functions.server.save_numbered_file(result['filename'] + '.' + extension_of_doc_format[doc_format], the_pdf_file, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence - for key in ('images', 'data_strings', 'convert_to_pdf_a', 'convert_to_tagged_pdf', 'password', 'owner_password', 'template_password', 'update_references', 'permissions', 'rendering_font'): + for key in ('images', 'data_strings', 'convert_to_pdf_a', 'use_pdftk', 'convert_to_tagged_pdf', 'password', 'owner_password', 'template_password', 'update_references', 'permissions', 'rendering_font'): if key in result: del result[key] docassemble.base.functions.reset_context() @@ -7167,14 +7180,14 @@ def prepare_attachment(self, attachment, the_user_dict): try: urlretrieve(url_sanitize(str(the_filename)), temp_template_file.name) except Exception as err: - raise DAError("prepare_attachment: error downloading " + str(the_filename) + ": " + str(err)) + raise DASourceError("prepare_attachment: error downloading " + str(the_filename) + ": " + str(err)) the_filename = temp_template_file.name else: the_filename = docassemble.base.functions.package_template_filename(the_filename, package=self.package) else: the_filename = None if the_filename is None or not os.path.isfile(the_filename): - raise DAError("prepare_attachment: error obtaining template file from code: " + repr(the_orig_filename)) + raise DASourceError("prepare_attachment: error obtaining template file from code: " + repr(the_orig_filename)) (the_base, actual_extension) = os.path.splitext(the_filename) # pylint: disable=unused-variable with open(the_filename, 'r', encoding='utf-8') as the_file: raw_content += the_file.read() @@ -7228,6 +7241,13 @@ def prepare_attachment(self, attachment, the_user_dict): result['convert_to_pdf_a'] = eval(attachment['options']['pdf_a'], the_user_dict) else: result['convert_to_pdf_a'] = self.interview.use_pdf_a + if 'pdftk' in attachment['options']: + if isinstance(attachment['options']['pdftk'], bool): + result['use_pdftk'] = attachment['options']['pdftk'] + else: + result['use_pdftk'] = eval(attachment['options']['pdftk'], the_user_dict) + else: + result['use_pdftk'] = self.interview.options.get('use pdftk', False) if 'rendering_font' in attachment['options']: result['rendering_font'] = attachment['options']['rendering_font'].text(the_user_dict).strip() else: @@ -7303,7 +7323,7 @@ def prepare_attachment(self, attachment, the_user_dict): for docx_reference in attachment['options']['docx_template_file']: for docx_path in docx_reference.paths(the_user_dict=the_user_dict): if docx_path is None or not os.path.isfile(docx_path): - raise DAError("Missing docx template file " + docx_reference.original_reference()) + raise DASourceError("Missing docx template file " + docx_reference.original_reference()) docx_paths.append(docx_path) if len(docx_paths) == 1: docx_path = docx_paths[0] @@ -7645,7 +7665,7 @@ def process_selections_manual(self, data): for key, value in sorted(data.items(), key=operator.itemgetter(1)): result.append({'key': TextObject(value, question=self), 'label': TextObject(key, question=self)}) else: - raise DAError("Unknown data type in manual choices selection: " + re.sub(r'[<>]', '', repr(data))) + raise DASourceError("Unknown data type in manual choices selection: " + re.sub(r'[<>]', '', repr(data))) return result @@ -8308,9 +8328,9 @@ def read_from(self, source): except Exception as errMess: self.success = False try: - error_to_raise = DAError(f'Error reading YAML file {source.path} in the block on line {line_number}\n\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n{format_yaml_errmess(errMess, source.path, line_number)}') + error_to_raise = DASourceError(f'Error reading YAML file {source.path} in the block on line {line_number}\n\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n{format_yaml_errmess(errMess, source.path, line_number)}') except: - error_to_raise = DAError(f'Error reading YAML file {source.path} in the block on line {line_number}\n\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n' + str(errMess.__class__.__name__)) + error_to_raise = DASourceError(f'Error reading YAML file {source.path} in the block on line {line_number}\n\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n' + str(errMess.__class__.__name__)) raise error_to_raise if document is not None: try: @@ -8318,13 +8338,13 @@ def read_from(self, source): self.names_used.update(question.fields_used) except SyntaxException as qError: self.success = False - raise DAError(f"Syntax Exception: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}") + raise DASourceError(f"Syntax Exception: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}") except CompileException as qError: self.success = False - raise DAError(f"Compile Exception: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}") + raise DASourceError(f"Compile Exception: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}") except SyntaxError as qError: self.success = False - raise DAError(f"Syntax Error: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}") + raise DASourceError(f"Syntax Error: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}") line_number += lines_in_code for ordering in self.id_orderings: if ordering['type'] == 'supersedes' and hasattr(ordering['question'], 'number'): @@ -8574,7 +8594,7 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q number_loops += 1 if number_loops > self.loop_limit: docassemble.base.functions.wrap_up() - raise DAError("There appears to be a circularity. Variables involved: " + ", ".join(variables_sought) + ".") + raise DASourceError("There appears to be a circularity. Variables involved: " + ", ".join(variables_sought) + ".") docassemble.base.functions.reset_gathering_mode() if 'action' in interview_status.current_info: # logmessage("assemble: there is an action in the current_info: " + repr(interview_status.current_info['action'])) @@ -8582,13 +8602,13 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q for the_key in ('list', 'item', 'items'): if the_key in interview_status.current_info['arguments']: if illegal_variable_name(interview_status.current_info['arguments'][the_key]): - raise DAError("Invalid name " + interview_status.current_info['arguments'][the_key]) + raise DASourceError("Invalid name " + interview_status.current_info['arguments'][the_key]) interview_status.current_info['action_' + the_key] = eval(interview_status.current_info['arguments'][the_key], user_dict) if interview_status.current_info['action'] in ('_da_dict_remove', '_da_dict_add', '_da_dict_complete'): for the_key in ('dict', 'item', 'items'): if the_key in interview_status.current_info['arguments']: if illegal_variable_name(interview_status.current_info['arguments'][the_key]): - raise DAError("Invalid name " + interview_status.current_info['arguments'][the_key]) + raise DASourceError("Invalid name " + interview_status.current_info['arguments'][the_key]) interview_status.current_info['action_' + the_key] = eval(interview_status.current_info['arguments'][the_key], user_dict) # else: # logmessage("assemble: there is no action in the current_info") @@ -8721,7 +8741,7 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q interview_status.populate(the_question.ask(user_dict, old_user_dict, 'None', [], None, None)) interview_status.mark_tentative_as_answered(user_dict) else: - raise DAError("An embedded question can only be a code block or a regular question block. The question type was " + getattr(the_question, 'question_type', 'unknown')) + raise DASourceError("An embedded question can only be a code block or a regular question block. The question type was " + getattr(the_question, 'question_type', 'unknown')) else: interview_status.populate(question.ask(user_dict, old_user_dict, 'None', [], None, None)) if interview_status.question.question_type == 'continue': @@ -8776,7 +8796,7 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q extra = " in " + errinfo.filename if hasattr(errinfo, 'lineno'): extra += " line " + str(errinfo.lineno) - raise DAError("NameError: " + str(the_exception) + extra) + raise DASourceError("NameError: " + str(the_exception) + extra) del cl del exc del tb @@ -8945,7 +8965,7 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q docassemble.base.functions.reset_context() # logmessage(str(the_error.args)) docassemble.base.functions.wrap_up() - raise DAError('Got error ' + str(the_error) + " " + traceback.format_exc() + "\nHistory was " + pprint.pformat(interview_status.seeking)) + raise DASourceError('Got error ' + str(the_error) + " " + traceback.format_exc() + "\nHistory was " + pprint.pformat(interview_status.seeking)) except MandatoryQuestion: # logmessage("MandatoryQuestion") docassemble.base.functions.reset_context() @@ -8967,8 +8987,8 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q pass docassemble.base.functions.wrap_up() if the_question is not None: - raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug))) - raise DAError("no question available: " + str(qError)) + raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug))) + raise DASourceError("no question available: " + str(qError)) except CompileException as qError: # logmessage("CompileException") docassemble.base.functions.reset_context() @@ -8979,8 +8999,8 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q pass docassemble.base.functions.wrap_up() if the_question is not None: - raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug))) - raise DAError("no question available: " + str(qError)) + raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug))) + raise DASourceError("no question available: " + str(qError)) else: docassemble.base.functions.wrap_up() raise DAErrorNoEndpoint('Docassemble has finished executing all code blocks marked as initial or mandatory, and finished asking all questions marked as mandatory (if any). It is a best practice to end your interview with a question that says goodbye and offers an Exit button.') @@ -9021,13 +9041,13 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, ** if self.debug: seeking.append({'variable': missingVariable, 'time': time.time()}) if recursion_depth > self.recursion_limit: - raise DAError("There appears to be an infinite loop. Variables in stack are " + ", ".join(sorted(variable_stack)) + ".") + raise DASourceError("There appears to be an infinite loop. Variables in stack are " + ", ".join(sorted(variable_stack)) + ".") # logmessage("askfor: I don't have " + str(missingVariable) + " for language " + str(language)) # logmessage("I don't have " + str(missingVariable) + " for language " + str(language)) origMissingVariable = missingVariable docassemble.base.functions.set_current_variable(origMissingVariable) # if missingVariable in variable_stack: - # raise DAError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack)) + # raise DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack)) # variable_stack.add(missingVariable) # found_generic = False # realMissingVariable = missingVariable @@ -9078,7 +9098,7 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, ** while True: num_cycles += 1 if num_cycles > self.loop_limit: - raise DAError("Infinite loop detected while looking for " + missing_var) + raise DASourceError("Infinite loop detected while looking for " + missing_var) a_question_was_skipped = False docassemble.base.functions.reset_gathering_mode(origMissingVariable) # logmessage("Starting the while loop") @@ -9370,14 +9390,14 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, ** try: urlretrieve(url_sanitize(str(the_filename)), temp_template_file.name) except Exception as err: - raise DAError("askfor: error downloading " + str(the_filename) + ": " + str(err)) + raise DASourceError("askfor: error downloading " + str(the_filename) + ": " + str(err)) the_filename = temp_template_file.name else: the_filename = docassemble.base.functions.package_template_filename(the_filename, package=question.package) else: the_filename = None if the_filename is None or not os.path.isfile(the_filename): - raise DAError("askfor: error obtaining template file from code: " + repr(the_orig_filename)) + raise DASourceError("askfor: error obtaining template file from code: " + repr(the_orig_filename)) with open(the_filename, 'r', encoding='utf-8') as the_file: raw_content += the_file.read() temp_vars = {} @@ -9530,7 +9550,7 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, ** continue return question.ask(user_dict, old_user_dict, the_x, iterators, missing_var, origMissingVariable) if a_question_was_skipped: - raise DAError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack)) + raise DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack)) if 'forgive_missing_question' in docassemble.base.functions.this_thread.misc and origMissingVariable in docassemble.base.functions.this_thread.misc['forgive_missing_question']: docassemble.base.functions.pop_current_variable() docassemble.base.functions.pop_event_stack(origMissingVariable) @@ -9601,7 +9621,7 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, ** extra = " in " + errinfo.filename if hasattr(errinfo, 'lineno'): extra += " line " + str(errinfo.lineno) - raise DAError("NameError: " + str(the_exception) + extra) + raise DASourceError("NameError: " + str(the_exception) + extra) del cl del exc del tb @@ -9611,7 +9631,7 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, ** raise # newMissingVariable = str(the_exception).split("'")[1] # if newMissingVariable in questions_tried and newMissingVariable in variable_stack: - # raise DAError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack)) + # raise DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack)) if newMissingVariable not in questions_tried: questions_tried[newMissingVariable] = set() else: @@ -9784,7 +9804,7 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, ** # logmessage("Got here 2") return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable} except: - # raise DAError("Problem setting that variable") + # raise DASourceError("Problem setting that variable") continue except SyntaxException as qError: # logmessage("SyntaxException") @@ -9795,8 +9815,8 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, ** except: pass if the_question is not None: - raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug))) - raise DAError("no question available in askfor: " + str(qError)) + raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug))) + raise DASourceError("no question available in askfor: " + str(qError)) except CompileException as qError: # logmessage("CompileException") docassemble.base.functions.reset_context() @@ -9806,8 +9826,8 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, ** except: pass if the_question is not None: - raise DAError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug))) - raise DAError("no question available in askfor: " + str(qError)) + raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug))) + raise DASourceError("no question available in askfor: " + str(qError)) # except SendFileError as qError: # # logmessage("Trapped SendFileError2") # question_data = {'extras': {}} @@ -9973,7 +9993,7 @@ def process_selections(data, exclude=None): else: logmessage("process_selections: non-label passed as label in dictionary") else: - raise DAError("Unknown data type in choices selection: " + re.sub(r'[<>]', '', repr(data))) + raise DASourceError("Unknown data type in choices selection: " + re.sub(r'[<>]', '', repr(data))) return result @@ -10079,7 +10099,7 @@ def ensure_object_exists(saveas, datatype, the_user_dict, commands=None): return parse_result = parse_var_name(saveas) if not parse_result['valid']: - raise DAError("Variable name " + saveas + " is invalid: " + parse_result['reason']) + raise DASourceError("Variable name " + saveas + " is invalid: " + parse_result['reason']) method = None if parse_result['final_parts'][1] != '': if parse_result['final_parts'][1][0] == '.': @@ -10732,7 +10752,7 @@ def register_jinja_filter(filter_name, func): def get_docx_variables(the_path): names = set() if not os.path.isfile(the_path): - raise DAError("Missing docx template file " + os.path.basename(the_path)) + raise DASourceError("Missing docx template file " + os.path.basename(the_path)) try: docx_template = DocxTemplate(the_path) docx_template.render_init() @@ -10743,7 +10763,7 @@ def get_docx_variables(the_path): the_xml = docx_template.patch_xml(the_xml) parsed_content = the_env.parse(the_xml) except Exception as the_err: - raise DAError("There was an error parsing the docx file: " + the_err.__class__.__name__ + " " + str(the_err)) + raise DASourceError("There was an error parsing the docx file: " + the_err.__class__.__name__ + " " + str(the_err)) for key in jinja2meta.find_undeclared_variables(parsed_content): if not key.startswith('__'): names.add(key) 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 @@ -160,7 +160,7 @@ def recursively_add_fields(fields, id_to_page, outfields, prefix='', parent_ft=N outfields.append((prefix, default, pageno, rect, field_type, export_value)) -def fill_template(template, data_strings=None, data_names=None, hidden=None, readonly=None, images=None, pdf_url=None, editable=True, pdfa=False, password=None, owner_password=None, template_password=None, default_export_value=None, replacement_font=None): +def fill_template(template, data_strings=None, data_names=None, hidden=None, readonly=None, images=None, pdf_url=None, editable=True, pdfa=False, password=None, owner_password=None, template_password=None, default_export_value=None, replacement_font=None, use_pdftk=False): if data_strings is None: data_strings = [] if data_names is None: @@ -219,7 +219,7 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea for key, val in data_strings: data_dict[key] = val pdf_file = tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".pdf", delete=False) - if pdfa or not editable: + if pdfa or not editable or use_pdftk: fdf = Xfdf(pdf_url, data_dict) # fdf = fdfgen.forge_fdf(pdf_url, data_strings, data_names, hidden, readonly) fdf_file = tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".xfdf", delete=False) @@ -251,7 +251,10 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea if len(images) > 0: subprocess_arguments.append('need_appearances') else: - subprocess_arguments.append('flatten') + if pdfa or not editable: + subprocess_arguments.append('flatten') + else: + subprocess_arguments.append('need_appearances') completed_process = None try: completed_process = subprocess.run(subprocess_arguments, timeout=600, check=False, capture_output=True) @@ -272,7 +275,6 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea pdf = Pdf.open(template, password=template_password) else: pdf = Pdf.open(template) - pdf.Root.AcroForm.NeedAppearances = True for page in pdf.pages: if not hasattr(page, 'Annots'): continue @@ -320,6 +322,9 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea the_string = pikepdf.String(value) annot.V = the_string annot.DV = the_string + pdf.Root.AcroForm.NeedAppearances = True + pdf.generate_appearance_streams() + pdf.Root.AcroForm.NeedAppearances = True if len(images) == 0: pdf.save(pdf_file.name) pdf.close() 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 @@ -2,6 +2,7 @@ from flask_wtf import FlaskForm from docassemble.base.functions import LazyWord as word from wtforms import validators, ValidationError, StringField, SubmitField, TextAreaField, SelectMultipleField, SelectField, FileField, HiddenField, RadioField, BooleanField +from docassemble.webapp.validators import html_validator import packaging @@ -37,21 +38,21 @@ def validate_package_name(form, field): # pylint: disable=unused-argument class CreatePackageForm(FlaskForm): name = StringField(word('Package name'), validators=[ - validators.DataRequired(word('Package name is required')), validate_name]) + validators.DataRequired(word('Package name is required')), validate_name, html_validator]) submit = SubmitField(word('Get template')) class CreatePlaygroundPackageForm(FlaskForm): name = SelectField(word('Package'), validators=[ - validators.DataRequired(word('Package name is required')), validate_name]) + validators.DataRequired(word('Package name is required')), validate_name, html_validator]) submit = SubmitField(word('Get package')) class UpdatePackageForm(FlaskForm): - giturl = StringField(word('GitHub URL')) - gitbranch = SelectField(word('GitHub Branch')) + giturl = StringField(word('GitHub URL'), validators=[html_validator]) + gitbranch = SelectField(word('GitHub Branch'), validators=[html_validator]) zipfile = FileField(word('Zip File')) - pippackage = StringField(word('Package on PyPI')) + pippackage = StringField(word('Package on PyPI'), validators=[html_validator]) submit = SubmitField(word('Update')) @@ -64,7 +65,7 @@ class ConfigForm(FlaskForm): class PlaygroundForm(FlaskForm): status = StringField('Status') original_playground_name = StringField(word('Original Name')) - playground_name = StringField(word('Name'), [validators.Length(min=1, max=255)]) + playground_name = StringField(word('Name'), [validators.Length(min=1, max=255), html_validator]) playground_content = TextAreaField(word('Playground YAML')) search_term = StringField(word('Search')) submit = SubmitField(word('Save')) @@ -108,7 +109,7 @@ class PlaygroundFilesEditForm(FlaskForm): purpose = StringField('Purpose') section = StringField(word('Section')) original_file_name = StringField(word('Original Name')) - file_name = StringField(word('Name'), [validators.Length(min=1, max=255)]) + file_name = StringField(word('Name'), [validators.Length(min=1, max=255), html_validator]) search_term = StringField(word('Search')) file_content = TextAreaField(word('File Text')) active_file = StringField(word('Active File')) @@ -118,7 +119,7 @@ class PlaygroundFilesEditForm(FlaskForm): class RenameProject(FlaskForm): name = StringField(word('New Name'), validators=[ - validators.DataRequired(word('Project name is required')), validate_project_name]) + validators.DataRequired(word('Project name is required')), validate_project_name, html_validator]) submit = SubmitField(word('Rename')) @@ -128,29 +129,29 @@ class DeleteProject(FlaskForm): class NewProject(FlaskForm): name = StringField(word('Name'), validators=[ - validators.DataRequired(word('Project name is required')), validate_project_name]) + validators.DataRequired(word('Project name is required')), validate_project_name, html_validator]) submit = SubmitField(word('Save')) class PullPlaygroundPackage(FlaskForm): - github_url = StringField(word('GitHub URL')) - github_branch = SelectField(word('GitHub Branch')) - pypi = StringField(word('PyPI package')) + github_url = StringField(word('GitHub URL'), validators=[html_validator]) + github_branch = SelectField(word('GitHub Branch'), validators=[html_validator]) + pypi = StringField(word('PyPI package'), validators=[html_validator]) pull = SubmitField(word('Pull')) cancel = SubmitField(word('Cancel')) class PlaygroundPackagesForm(FlaskForm): - original_file_name = StringField(word('Original Name')) + original_file_name = StringField(word('Original Name'), validators=[html_validator]) file_name = StringField(word('Package Name'), validators=[validators.Length(min=1, max=50), validators.DataRequired(word('Package Name is required')), - validate_package_name]) - license = StringField(word('License'), default='The MIT License (MIT)', validators=[validators.Length(min=0, max=255)]) - author_name = StringField(word('Author Name'), validators=[validators.Length(min=0, max=255)]) - author_email = StringField(word('Author E-mail'), validators=[validators.Length(min=0, max=255)]) - description = StringField(word('Description'), validators=[validators.Length(min=0, max=255)], default="A docassemble extension.") - version = StringField(word('Version'), validators=[validators.Length(min=0, max=255), validate_package_version], default="0.0.1") - url = StringField(word('URL'), validators=[validators.Length(min=0, max=255)], default="") + validate_package_name, html_validator]) + license = StringField(word('License'), default='The MIT License (MIT)', validators=[validators.Length(min=0, max=255), html_validator]) + author_name = StringField(word('Author Name'), validators=[validators.Length(min=0, max=255), html_validator]) + author_email = StringField(word('Author E-mail'), validators=[validators.Length(min=0, max=255), html_validator]) + description = StringField(word('Description'), validators=[validators.Length(min=0, max=255), html_validator], default="A docassemble extension.") + version = StringField(word('Version'), validators=[validators.Length(min=0, max=255), validate_package_version, html_validator], default="0.0.1") + url = StringField(word('URL'), validators=[validators.Length(min=0, max=255), html_validator], default="") dependencies = SelectMultipleField(word('Dependencies')) interview_files = SelectMultipleField(word('Interview files')) template_files = SelectMultipleField(word('Template files')) @@ -159,7 +160,7 @@ class PlaygroundPackagesForm(FlaskForm): sources_files = SelectMultipleField(word('Source files')) readme = TextAreaField(word('README file'), default='') github_branch = NonValidatingSelectField(word('Branch')) - github_branch_new = StringField(word('Name of new branch')) + github_branch_new = StringField(word('Name of new branch'), validators=[html_validator]) commit_message = StringField(word('Commit message'), default="") pypi_also = BooleanField(word('Publish on PyPI also')) install_also = BooleanField(word('Install package on this server also')) @@ -222,7 +223,7 @@ class APIKey(FlaskForm): action = HiddenField() key = HiddenField() security = HiddenField() - name = StringField(word('Name'), validators=[validators.Length(min=1, max=255)]) + name = StringField(word('Name'), validators=[validators.Length(min=1, max=255), html_validator]) method = SelectField(word('Security Method')) permissions = SelectMultipleField(word('Limited Permissions')) submit = SubmitField(word('Create')) 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 @@ -49,7 +49,7 @@ from docassemble.webapp.setup import da_version import docassemble.base.astparser from docassemble.webapp.api_key import encrypt_api_key -from docassemble.base.error import DAError, DAErrorNoEndpoint, DAErrorMissingVariable, DAErrorCompileError, DAValidationError, DAException, DANotFoundError, DAInvalidFilename +from docassemble.base.error import DAError, DAErrorNoEndpoint, DAErrorMissingVariable, DAErrorCompileError, DAValidationError, DAException, DANotFoundError, DAInvalidFilename, DASourceError import docassemble.base.functions from docassemble.base.functions import get_default_timezone, ReturnValue, word import docassemble.base.DA @@ -159,7 +159,6 @@ docassemble.base.util.set_svm_machine_learner(docassemble.webapp.machinelearning.SVMMachineLearner) - min_system_version = '1.2.0' re._MAXCACHE = 10000 @@ -1178,12 +1177,17 @@ def my_default_url(error, endpoint, values): # pylint: disable=unused-argument def make_safe_url(url): + if url in ('help', 'login', 'signin', 'restart', 'new_session', 'exit', 'interview', 'logout', 'exit_logout', 'leave', 'register', 'profile', 'change_password', 'interviews', 'dispatch', 'manage', 'config', 'playground', 'playgroundtemplate', 'playgroundstatic', 'playgroundsources', 'playgroundmodules', 'playgroundpackages', 'configuration', 'root', 'temp_url', 'login_url', 'exit_endpoint', 'interview_start', 'interview_list', 'playgroundfiles', 'create_playground_package', 'run', 'run_interview_in_package', 'run_dispatch', 'run_new', 'run_new_dispatch'): + return url parts = urlsplit(url) safe_url = parts.path if parts.query != '': safe_url += '?' + parts.query if parts.fragment != '': safe_url += '#' + parts.fragment + if len(safe_url) > 0 and safe_url[0] not in ('?', '#', '/'): + safe_url = '/' + safe_url + safe_url = re.sub(r'^//+', '/', safe_url) return safe_url @@ -1238,6 +1242,7 @@ def password_validator(form, field): # pylint: disable=unused-argument if DEBUG_BOOT: boot_log("server: finished setting up Flask") + def url_for_interview(**args): for k, v in daconfig.get('dispatch').items(): if v == args['i']: @@ -5840,7 +5845,7 @@ def github_oauth_callback(): return ('File not found', 404) setup_translation() failed = False - do_redirect = False + do_a_redirect = False if not app.config['USE_GITHUB']: logmessage('github_oauth_callback: server does not use github') failed = True @@ -5853,14 +5858,14 @@ def github_oauth_callback(): if 'code' not in request.args or 'state' not in request.args: logmessage('github_oauth_callback: code and state not in args') failed = True - do_redirect = True + do_a_redirect = True elif request.args['state'] != github_next['state']: logmessage('github_oauth_callback: state did not match') failed = True if failed: r.delete('da:github:userid:' + str(current_user.id)) r.delete('da:using_github:userid:' + str(current_user.id)) - if do_redirect: + if do_a_redirect: flash(word("There was a problem connecting to GitHub. Please check your GitHub configuration and try again."), 'danger') return redirect(url_for('github_menu')) return ('File not found', 404) @@ -8265,7 +8270,7 @@ def index(action_argument=None, refer=None): the_field = validation_error.field logmessage("field is " + the_field) if the_field not in key_to_orig_key: - for item in key_to_orig_key.keys(): + for item in key_to_orig_key: if item.startswith(the_field + '['): the_field = item break @@ -10915,7 +10920,7 @@ def index(action_argument=None, refer=None): $(query).each(function(){ var showIfParent = $(this).parents('.dashowif,.dajsshowif'); if (!(showIfParent.length && ($(showIfParent[0]).data('isVisible') == '0' || !$(showIfParent[0]).is(":visible")))){ - if ($(this).hasClass('combobox')){ + if ($(this).prop('tagName') == 'INPUT' && $(this).hasClass('combobox')){ if (value){ daComboBoxes[$(this).attr('id')].disable(); } @@ -14001,7 +14006,7 @@ def observer(): $(query).each(function(){ var showIfParent = $(this).parents('.dashowif, .dajsshowif'); if (!(showIfParent.length && ($(showIfParent[0]).data('isVisible') == '0' || !$(showIfParent[0]).is(":visible")))){ - if ($(this).hasClass('combobox')){ + if ($(this).prop('tagName') == 'INPUT' && $(this).hasClass('combobox')){ if (value){ daComboBoxes[$(this).attr('id')].disable(); } @@ -23043,7 +23048,14 @@ def server_error(the_error): else: the_history = None the_vars = None - if isinstance(the_error, (DAError, DANotFoundError, DAInvalidFilename)): + if isinstance(the_error, DASourceError): + if (DEBUG and daconfig.get('development site is protected', False)) or (current_user.is_authenticated and current_user.has_role('admin', 'developer')): + errmess = str(the_error) + else: + errmess = word("There was an error. Please contact the system administrator.") + the_trace = None + logmessage(str(the_error)) + elif isinstance(the_error, (DAError, DANotFoundError, DAInvalidFilename)): errmess = str(the_error) the_trace = None logmessage(errmess) @@ -23073,7 +23085,10 @@ def server_error(the_error): errmess += "\nIn field index number " + str(docassemble.base.functions.this_thread.misc['current_field']) if hasattr(the_error, 'da_line_with_error'): errmess += "\nIn line: " + str(the_error.da_line_with_error) - + try: + logmessage(errmess) + except: + logmessage("Could not log the error message") logmessage(the_trace) if isinstance(the_error, DAError): error_code = the_error.error_code @@ -23296,7 +23311,7 @@ def server_error(the_error): if 'in error' not in session and docassemble.base.functions.this_thread.interview is not None and 'error action' in docassemble.base.functions.this_thread.interview.consolidated_metadata: session['in error'] = True return index(action_argument={'action': docassemble.base.functions.this_thread.interview.consolidated_metadata['error action'], 'arguments': {'error_message': orig_errmess, 'error_history': the_history, 'error_trace': the_trace}}, refer=['error']) - show_debug = not bool((not DEBUG) and isinstance(the_error, (DAError, DAInvalidFilename))) + show_debug = not bool((not (DEBUG and daconfig.get('development site is protected', False))) and isinstance(the_error, (DAError, DAInvalidFilename))) if int(int(error_code)/100) == 4: show_debug = False if error_code == 404: @@ -27000,6 +27015,7 @@ def invite_user(email_address, privilege=None, send=True): return None return accept_invite_link + @app.route('/api/user_invite', methods=['POST']) @csrf.exempt @cross_origin(origins='*', methods=['POST', 'HEAD'], automatic_options=True) 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 @@ -5,6 +5,9 @@ from wtforms import DateField, StringField, SubmitField, ValidationError, BooleanField, SelectField, SelectMultipleField, HiddenField, validators, TextAreaField from wtforms.validators import DataRequired, Email, Optional from wtforms.widgets import PasswordInput +from flask import flash, current_app, request, abort +from flask_login import current_user +from sqlalchemy import select from docassemble.base.functions import LazyWord as word, LazyArray from docassemble.base.config import daconfig from docassemble.base.generate_key import random_alphanumeric @@ -12,9 +15,7 @@ from docassemble.webapp.daredis import r from docassemble.webapp.db_object import db from docassemble.webapp.users.models import UserModel, Role -from flask import flash, current_app, request, abort -from flask_login import current_user -from sqlalchemy import select +from docassemble.webapp.validators import html_validator try: import ldap except ImportError: @@ -207,21 +208,22 @@ def da_registration_restrict_validator(form, field): # pylint: disable=unused-a class MyRegisterForm(RegisterForm): - first_name = StringField(word('First name'), [validators.Length(min=0, max=255)]) - last_name = StringField(word('Last name'), [validators.Length(min=0, max=255)]) - country = StringField(word('Country code'), [validators.Length(min=0, max=2)]) - subdivisionfirst = StringField(word('First subdivision'), [validators.Length(min=0, max=64)]) - subdivisionsecond = StringField(word('Second subdivision'), [validators.Length(min=0, max=64)]) - subdivisionthird = StringField(word('Third subdivision'), [validators.Length(min=0, max=64)]) - organization = StringField(word('Organization'), [validators.Length(min=0, max=64)]) - language = StringField(word('Language'), [validators.Length(min=0, max=64)]) - timezone = SelectField(word('Time Zone'), [validators.Length(min=0, max=64)]) - nickname = StringField(word('Nickname'), [fix_nickname]) + first_name = StringField(word('First name'), [validators.Length(min=0, max=255), html_validator]) + last_name = StringField(word('Last name'), [validators.Length(min=0, max=255), html_validator]) + country = StringField(word('Country code'), [validators.Length(min=0, max=2), html_validator]) + subdivisionfirst = StringField(word('First subdivision'), [validators.Length(min=0, max=64), html_validator]) + subdivisionsecond = StringField(word('Second subdivision'), [validators.Length(min=0, max=64), html_validator]) + subdivisionthird = StringField(word('Third subdivision'), [validators.Length(min=0, max=64), html_validator]) + organization = StringField(word('Organization'), [validators.Length(min=0, max=64), html_validator]) + language = StringField(word('Language'), [validators.Length(min=0, max=64), html_validator]) + timezone = SelectField(word('Time Zone'), [validators.Length(min=0, max=64), html_validator]) + nickname = StringField(word('Nickname'), [fix_nickname, html_validator]) email = StringField(word('Email'), validators=[ validators.DataRequired(word('Email is required')), validators.Email(word('Invalid Email')), da_unique_email_validator, - da_registration_restrict_validator]) + da_registration_restrict_validator, + html_validator]) def length_two(form, field): # pylint: disable=unused-argument @@ -236,16 +238,16 @@ class NewPrivilegeForm(FlaskForm): class UserProfileForm(FlaskForm): - first_name = StringField(word('First name'), [validators.Length(min=0, max=255)]) - last_name = StringField(word('Last name'), [validators.Length(min=0, max=255)]) - country = StringField(word('Country code'), [validators.Length(min=0, max=2)]) - subdivisionfirst = StringField(word('First subdivision'), [validators.Length(min=0, max=64)]) - subdivisionsecond = StringField(word('Second subdivision'), [validators.Length(min=0, max=64)]) - subdivisionthird = StringField(word('Third subdivision'), [validators.Length(min=0, max=64)]) - organization = StringField(word('Organization'), [validators.Length(min=0, max=64)]) - language = StringField(word('Language'), [validators.Length(min=0, max=64)]) - timezone = SelectField(word('Time Zone'), [validators.Length(min=0, max=64)]) - pypi_username = StringField(word('PyPI Username'), [validators.Length(min=0, max=255)]) + first_name = StringField(word('First name'), [validators.Length(min=0, max=255), html_validator]) + last_name = StringField(word('Last name'), [validators.Length(min=0, max=255), html_validator]) + country = StringField(word('Country code'), [validators.Length(min=0, max=2), html_validator]) + subdivisionfirst = StringField(word('First subdivision'), [validators.Length(min=0, max=64), html_validator]) + subdivisionsecond = StringField(word('Second subdivision'), [validators.Length(min=0, max=64), html_validator]) + subdivisionthird = StringField(word('Third subdivision'), [validators.Length(min=0, max=64), html_validator]) + organization = StringField(word('Organization'), [validators.Length(min=0, max=64), html_validator]) + language = StringField(word('Language'), [validators.Length(min=0, max=64), html_validator]) + timezone = SelectField(word('Time Zone'), [validators.Length(min=0, max=64), html_validator]) + pypi_username = StringField(word('PyPI Username'), [validators.Length(min=0, max=255), html_validator]) pypi_password = StringField(word('PyPI Password'), [validators.Length(min=0, max=255)]) confirmed_at = DateField(word('Confirmation Date')) submit = SubmitField(word('Save')) @@ -253,7 +255,7 @@ class UserProfileForm(FlaskForm): class EditUserProfileForm(UserProfileForm): - email = StringField(word('E-mail')) + email = StringField(word('E-mail'), validators=[Email(word('Must be a valid e-mail address')), html_validator]) role_id = SelectMultipleField(word('Privileges'), coerce=int) active = BooleanField(word('Active')) uses_mfa = BooleanField(word('Uses two-factor authentication')) @@ -299,11 +301,11 @@ def validate(self): # pylint: disable=arguments-differ flash(word("Please choose a different e-mail address."), 'error') return False return super().validate() - email = StringField(word('E-mail'), validators=[Optional(), Email(word('Must be a valid e-mail address'))]) + email = StringField(word('E-mail'), validators=[Optional(), Email(word('Must be a valid e-mail address')), html_validator]) class RequestDeveloperForm(FlaskForm): - reason = StringField(word('Reason for needing developer account (optional)')) + reason = StringField(word('Reason for needing developer account (optional)'), validators=[html_validator]) submit = SubmitField(word('Submit')) @@ -334,21 +336,21 @@ class UserAddForm(FlaskForm): email = StringField(word('E-mail'), validators=[ validators.InputRequired(word('E-mail is required')), validators.Email(word('Invalid E-mail'))]) - first_name = StringField(word('First name'), [validators.Length(min=0, max=255)]) - last_name = StringField(word('Last name'), [validators.Length(min=0, max=255)]) + first_name = StringField(word('First name'), [validators.Length(min=0, max=255), html_validator]) + last_name = StringField(word('Last name'), [validators.Length(min=0, max=255), html_validator]) role_id = SelectMultipleField(word('Privileges'), coerce=int) password = StringField(word('Password'), widget=PasswordInput(hide_value=False), validators=[password_validator]) submit = SubmitField(word('Add')) class PhoneLoginForm(FlaskForm): - phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255)]) + phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255), html_validator]) submit = SubmitField(word('Go')) class PhoneLoginVerifyForm(FlaskForm): - phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255)]) - verification_code = StringField(word('Verification code'), [validators.Length(min=daconfig['verification code digits'], max=daconfig['verification code digits'])]) + phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255), html_validator]) + verification_code = StringField(word('Verification code'), [validators.Length(min=daconfig['verification code digits'], max=daconfig['verification code digits']), html_validator]) submit = SubmitField(word('Verify')) def validate(self): # pylint: disable=arguments-differ @@ -404,7 +406,7 @@ class MFAChooseForm(FlaskForm): class MFASMSSetupForm(FlaskForm): - phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255)]) + phone_number = StringField(word('Phone number'), [validators.Length(min=5, max=255), html_validator]) submit = SubmitField(word('Verify')) @@ -416,8 +418,8 @@ class MFAVerifySMSSetupForm(FlaskForm): class MyResendConfirmEmailForm(FlaskForm): email = StringField(word('Your e-mail address'), validators=[ validators.DataRequired(word('E-mail address is required')), - validators.Email(word('Invalid e-mail address')), - ]) + validators.Email(word('Invalid e-mail address')) + ]) submit = SubmitField(word('Send confirmation email')) 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'))