')
-
-
-def set_default_page_width(width):
- global DEFAULT_PAGE_WIDTH
- DEFAULT_PAGE_WIDTH = str(width)
-
-
-def get_default_page_width():
- return DEFAULT_PAGE_WIDTH
-
-DEFAULT_IMAGE_WIDTH = '4in'
-
-
-def set_default_image_width(width):
- global DEFAULT_IMAGE_WIDTH
- DEFAULT_IMAGE_WIDTH = str(width)
-
-
-def get_default_image_width():
- return DEFAULT_IMAGE_WIDTH
-
-MAX_HEIGHT_POINTS = 10 * 72
-
-
-def set_max_height_points(points):
- global MAX_HEIGHT_POINTS
- MAX_HEIGHT_POINTS = points
-
-
-def get_max_height_points():
- return MAX_HEIGHT_POINTS
-
-MAX_WIDTH_POINTS = 6.5 * 72.0
-
-
-def set_max_width_points(points):
- global MAX_WIDTH_POINTS
- MAX_WIDTH_POINTS = points
-
-
-def get_max_width_points():
- return MAX_WIDTH_POINTS
-
-# def blank_da_send_mail(*args, **kwargs):
-# logmessage("da_send_mail: no mail agent configured!")
-# return(None)
-
-# da_send_mail = blank_da_send_mail
-
-# def set_da_send_mail(func):
-# global da_send_mail
-# da_send_mail = func
-# return
-
-# def blank_file_finder(*args, **kwargs):
-# return({'filename': "invalid"})
-
-# file_finder = blank_file_finder
-
-# def set_file_finder(func):
-# global file_finder
-# #logmessage("set the file finder to " + str(func))
-# file_finder = func
-# return
-
-# def blank_url_finder(*args, **kwargs):
-# return('about:blank')
-
-# url_finder = blank_url_finder
-
-# def set_url_finder(func):
-# global url_finder
-# url_finder = func
-# return
-
-# def blank_url_for(*args, **kwargs):
-# return('about:blank')
-
-# url_for = blank_url_for
-
-# def set_url_for(func):
-# global url_for
-# url_for = func
-# return
-
-rtf_spacing = {'tight': r'\\sl0 ', 'single': r'\\sl0 ', 'oneandahalf': r'\\sl360\\slmult1 ', 'double': r'\\sl480\\slmult1 ', 'triple': r'\\sl720\\slmult1 '}
-
-rtf_after_space = {'tight': 0, 'single': 1, 'oneandahalf': 0, 'double': 0, 'triplespacing': 0, 'triple': 0}
-
-
-def rtf_prefilter(text):
- text = re.sub(r'^# ', '[HEADING1] ', text, flags=re.MULTILINE)
- text = re.sub(r'^## ', '[HEADING2] ', text, flags=re.MULTILINE)
- text = re.sub(r'^### ', '[HEADING3] ', text, flags=re.MULTILINE)
- text = re.sub(r'^#### ', '[HEADING4] ', text, flags=re.MULTILINE)
- text = re.sub(r'^##### ', '[HEADING5] ', text, flags=re.MULTILINE)
- text = re.sub(r'^###### ', '[HEADING6] ', text, flags=re.MULTILINE)
- text = re.sub(r'^####### ', '[HEADING7] ', text, flags=re.MULTILINE)
- text = re.sub(r'^######## ', '[HEADING8] ', text, flags=re.MULTILINE)
- text = re.sub(r'^######### ', '[HEADING9] ', text, flags=re.MULTILINE)
- text = re.sub(r'\s*\[VERTICAL_LINE\]\s*', '\n\n[VERTICAL_LINE]\n\n', text)
- text = re.sub(r'\s*\[BREAK\]\s*', '\n\n[BREAK]\n\n', text)
- text = re.sub(r'\s+\[END_TWOCOL\]', '\n\n[END_TWOCOL]', text)
- text = re.sub(r'\s+\[END_CAPTION\]', '\n\n[END_CAPTION]', text)
- text = re.sub(r'\[BEGIN_TWOCOL\]\s+', '[BEGIN_TWOCOL]\n\n', text)
- text = re.sub(r'\[BEGIN_CAPTION\]\s+', '[BEGIN_CAPTION]\n\n', text)
- return text
-
-
-def repeat_along(chars, match):
- output = chars * len(match.group(1))
- # logmessage("Output is " + repr(output))
- return output
-
-
-def rtf_filter(text, metadata=None, styles=None, question=None):
- if metadata is None:
- metadata = {}
- if styles is None:
- styles = {}
- # logmessage(text)
- if 'fontsize' in metadata:
- text = re.sub(r'{\\pard', r'\\fs' + str(convert_length(metadata['fontsize'], 'hp')) + r' {\\pard', text, count=1)
- after_space_multiplier = convert_length(metadata['fontsize'], 'twips')
- else:
- after_space_multiplier = 240
- if 'IndentationAmount' in metadata:
- indentation_amount = str(convert_length(metadata['IndentationAmount'], 'twips'))
- else:
- indentation_amount = '720'
- if 'Indentation' in metadata:
- default_indentation = bool(metadata['Indentation'])
- else:
- default_indentation = True
- if 'SingleSpacing' in metadata and metadata['SingleSpacing']:
- # logmessage("Gi there!")
- default_spacing = 'single'
- if 'Indentation' not in metadata:
- default_indentation = False
- elif 'OneAndAHalfSpacing' in metadata and metadata['OneAndAHalfSpacing']:
- default_spacing = 'oneandahalf'
- elif 'DoubleSpacing' in metadata and metadata['DoubleSpacing']:
- default_spacing = 'double'
- elif 'TripleSpacing' in metadata and metadata['TripleSpacing']:
- default_spacing = 'triple'
- else:
- default_spacing = 'double'
- after_space = after_space_multiplier * rtf_after_space[default_spacing]
- text = re.sub(r'{\\pard \\ql \\f0 \\sa180 \\li0 \\fi0 \[HEADING([0-9]+)\] *', (lambda x: '{\\pard ' + styles.get(x.group(1), '\\ql \\f0 \\sa180 \\li0 \\fi0 ')), text)
- text = re.sub(r'{\\pard \\ql \\f0 \\sa180 \\li0 \\fi0 \[(BEGIN_TWOCOL|BREAK|END_TWOCOL|BEGIN_CAPTION|VERTICAL_LINE|END_CAPTION|TIGHTSPACING|SINGLESPACING|DOUBLESPACING|START_INDENTATION|STOP_INDENTATION|PAGEBREAK|SKIPLINE|NOINDENT|FLUSHLEFT|FLUSHRIGHT|CENTER|BOLDCENTER|INDENTBY[^\]]*)\] *', r'[\1]{\\pard \\ql \\f0 \\sa180 \\li0 \\fi0 ', text)
- text = re.sub(r'{\\pard \\ql \\f0 \\sa180 \\li0 \\fi0 *\\par}', r'', text)
- text = re.sub(r'\[\[([^\]]*)\]\]', r'\1', text)
- # with open('/tmp/asdf.rtf', 'w') as deb_file:
- # deb_file.write(text)
- text = re.sub(r'\\par}\s*\[(END_TWOCOL|END_CAPTION|BREAK|VERTICAL_LINE)\]', r'}[\1]', text, flags=re.DOTALL)
- text = re.sub(r'\[BEGIN_TWOCOL\](.+?)\s*\[BREAK\]\s*(.+?)\[END_TWOCOL\]', rtf_two_col, text, flags=re.DOTALL)
- text = re.sub(r'\[EMOJI ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_as_rtf(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', lambda x: image_as_rtf(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_as_rtf(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+)\]', lambda x: image_as_rtf(x, question=question), text)
- text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', qr_as_rtf, text)
- text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+)\]', qr_as_rtf, text)
- text = re.sub(r'\[QR ([^\]]+)\]', qr_as_rtf, text)
- text = re.sub(r'\[MAP ([^\]]+)\]', '', text)
- text = replace_fields(text)
- # text = re.sub(r'\[FIELD ([^\]]+)\]', '', text)
- text = re.sub(r'\[TARGET ([^\]]+)\]', '', text)
- text = re.sub(r'\[YOUTUBE[^ ]* ([^\]]+)\]', '', text)
- text = re.sub(r'\[VIMEO[^ ]* ([^\]]+)\]', '', text)
- text = re.sub(r'\[BEGIN_CAPTION\](.+?)\s*\[VERTICAL_LINE\]\s*(.+?)\[END_CAPTION\]', rtf_caption_table, text, flags=re.DOTALL)
- text = re.sub(r'\[NBSP\]', r'\\~ ', text)
- text = re.sub(r'\[REDACTION_SPACE\]', r'\\u9608\\zwbo', text)
- text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', lambda x: repeat_along('\\u9608', x), text)
- text = re.sub(r'\[ENDASH\]', r'{\\endash}', text)
- text = re.sub(r'\[EMDASH\]', r'{\\emdash}', text)
- text = re.sub(r'\[HYPHEN\]', r'-', text)
- text = re.sub(r'\[CHECKBOX\]', r'____', text)
- text = re.sub(r'\[BLANK\]', r'________________', text)
- text = re.sub(r'\[BLANKFILL\]', r'________________', text)
- text = re.sub(r'\[PAGEBREAK\] *', r'\\page ', text)
- text = re.sub(r'\[PAGENUM\]', r'{\\chpgn}', text)
- text = re.sub(r'\[TOTALPAGES\]', r'{\\field{\\*\\fldinst NUMPAGES } {\\fldrslt 1}}', text)
- text = re.sub(r'\[SECTIONNUM\]', r'{\\sectnum}', text)
- text = re.sub(r' *\[SKIPLINE\] *', r'\\line ', text)
- text = re.sub(r' *\[NEWLINE\] *', r'\\line ', text)
- text = re.sub(r' *\[NEWPAR\] *', r'\\par ', text)
- text = re.sub(r' *\[BR\] *', r'\\line ', text)
- text = re.sub(r' *\[TAB\] *', r'\\tab ', text)
- text = re.sub(r' *\[END\] *', r'\n', text)
- text = re.sub(r'\\sa180\\sa180\\par', r'\\par', text)
- text = re.sub(r'\\sa180', r'\\sa0', text)
- text = re.sub(r'(\\trowd \\trgaph[0-9]+)', r'\1\\trqc', text)
- text = re.sub(r'\\intbl\\row}\s*{\\pard', r'\\intbl\\row}\n\\line\n{\\pard', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\s*\[(SAVE|RESTORE|TIGHTSPACING|SINGLESPACING|DOUBLESPACING|TRIPLESPACING|ONEANDAHALFSPACING|START_INDENTATION|STOP_INDENTATION)\]\s*', r'\n[\1]\n', text)
- lines = text.split('\n')
- spacing_command = rtf_spacing[default_spacing]
- if default_indentation:
- indentation_command = r'\\fi' + str(indentation_amount) + " "
- else:
- indentation_command = r'\\fi0 '
- text = ''
- formatting_stack = []
- for line in lines:
- if re.search(r'\[SAVE\]', line):
- formatting_stack.append({'spacing_command': spacing_command, 'after_space': after_space, 'default_indentation': default_indentation, 'indentation_command': indentation_command})
- elif re.search(r'\[RESTORE\]', line):
- if len(formatting_stack) > 0:
- prior_values = formatting_stack.pop()
- spacing_command = prior_values['spacing_command']
- after_space = prior_values['after_space']
- default_indentation = prior_values['default_indentation']
- indentation_command = prior_values['indentation_command']
- elif re.search(r'\[TIGHTSPACING\]', line):
- spacing_command = rtf_spacing['tight']
- default_spacing = 'tight'
- after_space = after_space_multiplier * rtf_after_space[default_spacing]
- default_indentation = False
- elif re.search(r'\[SINGLESPACING\]', line):
- spacing_command = rtf_spacing['single']
- default_spacing = 'single'
- after_space = after_space_multiplier * rtf_after_space[default_spacing]
- default_indentation = False
- elif re.search(r'\[ONEANDAHALFSPACING\]', line):
- spacing_command = rtf_spacing['oneandahalf']
- default_spacing = 'oneandahalf'
- after_space = after_space_multiplier * rtf_after_space[default_spacing]
- elif re.search(r'\[DOUBLESPACING\]', line):
- spacing_command = rtf_spacing['double']
- default_spacing = 'double'
- after_space = after_space_multiplier * rtf_after_space[default_spacing]
- elif re.search(r'\[TRIPLESPACING\]', line):
- spacing_command = rtf_spacing['triple']
- default_spacing = 'triple'
- after_space = after_space_multiplier * rtf_after_space[default_spacing]
- elif re.search(r'\[START_INDENTATION\]', line):
- indentation_command = r'\\fi' + str(indentation_amount) + " "
- elif re.search(r'\[STOP_INDENTATION\]', line):
- indentation_command = r'\\fi0 '
- elif line != '':
- special_after_space = None
- special_spacing = None
- if re.search(r'\[BORDER\]', line):
- line = re.sub(r' *\[BORDER\] *', r'', line)
- border_text = r'\\box \\brdrhair \\brdrw1 \\brdrcf1 \\brsp29 '
- else:
- border_text = r''
- line = re.sub(r'{(\\pard\\intbl \\q[lrc] \\f[0-9]+ \\sa[0-9]+ \\li[0-9]+ \\fi[0-9]+.*?)\\par}', r'\1', line)
- if re.search(r'\[NOPAR\]', line):
- line = re.sub(r'{\\pard \\ql \\f[0-9]+ \\sa[0-9]+ \\li[0-9]+ \\fi-?[0-9]* *(.*?)\\par}', r'\1', line)
- line = re.sub(r' *\[NOPAR\] *', r'', line)
- n = re.search(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+) *([0-9\.]+ *[A-Za-z]+)\]', line)
- m = re.search(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+)\]', line)
- if n:
- line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
- line = re.sub(r'\\ri-?[0-9]+ ', r'', line)
- line = re.sub(r'\\li-?[0-9]+ ', r'\\li' + str(convert_length(n.group(1), 'twips')) + r' \\ri' + str(convert_length(n.group(2), 'twips')) + ' ', line)
- line = re.sub(r'\[INDENTBY[^\]]*\]', '', line)
- elif m:
- line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
- line = re.sub(r'\\li-?[0-9]+ ', r'\\li' + str(convert_length(m.group(1), 'twips')) + ' ', line)
- line = re.sub(r' *\[INDENTBY[^\]]*\] *', '', line)
- elif re.search(r'\[NOINDENT\]', line):
- line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
- line = re.sub(r' *\[NOINDENT\] *', '', line)
- elif re.search(r'\[FLUSHLEFT\]', line):
- line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
- line = re.sub(r' *\[FLUSHLEFT\] *', '', line)
- special_after_space = after_space_multiplier * 1
- special_spacing = rtf_spacing['single']
- elif re.search(r'\[FLUSHRIGHT\]', line):
- line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
- line = re.sub(r'\\ql', r'\\qr', line)
- line = re.sub(r' *\[FLUSHRIGHT\] *', '', line)
- special_after_space = after_space_multiplier * 1
- special_spacing = rtf_spacing['single']
- elif re.search(r'\[CENTER\]', line):
- line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
- line = re.sub(r'\\ql', r'\\qc', line)
- line = re.sub(r' *\[CENTER\] *', '', line)
- elif re.search(r'\[BOLDCENTER\]', line):
- line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
- line = re.sub(r'\\ql', r'\\qc \\b', line)
- line = re.sub(r' *\[BOLDCENTER\] *', '', line)
- elif indentation_command != '' and not re.search(r'\\widctlpar', line):
- line = re.sub(r'\\fi-?[0-9]+ ', indentation_command, line)
- if not re.search(r'\\s[0-9]', line):
- if special_spacing:
- spacing_command_to_use = special_spacing
- else:
- spacing_command_to_use = spacing_command
- line = re.sub(r'\\pard ', r'\\pard ' + str(spacing_command_to_use) + str(border_text), line)
- line = re.sub(r'\\pard\\intbl ', r'\\pard\\intbl ' + str(spacing_command_to_use) + str(border_text), line)
- if not (re.search(r'\\fi0\\(endash|bullet)', line) or re.search(r'\\s[0-9]', line) or re.search(r'\\intbl', line)):
- if special_after_space:
- after_space_to_use = special_after_space
- else:
- after_space_to_use = after_space
- if after_space_to_use > 0:
- line = re.sub(r'\\sa[0-9]+ ', r'\\sa' + str(after_space_to_use) + ' ', line)
- else:
- line = re.sub(r'\\sa[0-9]+ ', r'\\sa0 ', line)
- text += line + '\n'
- text = re.sub(r'{\\pard \\sl[0-9]+\\slmult[0-9]+ \\ql \\f[0-9]+ \\sa[0-9]+ \\li[0-9]+ \\fi-?[0-9]*\s*\\par}', r'', text)
- text = re.sub(r'\[MANUALSKIP\]', r'{\\pard \\sl0 \\ql \\f0 \\sa0 \\li0 \\fi0 \\par}', text)
- return text
-
-
-def docx_filter(text, metadata=None, question=None):
- if metadata is None:
- metadata = {}
- text = text + "\n\n"
- text = re.sub(r'\[\[([^\]]*)\]\]', r'\1', text)
- text = re.sub(r'\[EMOJI ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_docx(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', lambda x: image_include_docx(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_docx(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+)\]', lambda x: image_include_docx(x, question=question), text)
- text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', qr_include_docx, text)
- text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+)\]', qr_include_docx, text)
- text = re.sub(r'\[QR ([^\]]+)\]', qr_include_docx, text)
- text = re.sub(r'\[MAP ([^\]]+)\]', '', text)
- text = replace_fields(text)
- # text = re.sub(r'\[FIELD ([^\]]+)\]', '', text)
- text = re.sub(r'\[TARGET ([^\]]+)\]', '', text)
- text = re.sub(r'\[YOUTUBE[^ ]* ([^\]]+)\]', '', text)
- text = re.sub(r'\[VIMEO[^ ]* ([^\]]+)\]', '', text)
- text = re.sub(r'\\clearpage *\\clearpage', '', text)
- text = re.sub(r'\[START_INDENTATION\]', '', text)
- text = re.sub(r'\[STOP_INDENTATION\]', '', text)
- text = re.sub(r'\[BEGIN_CAPTION\](.+?)\[VERTICAL_LINE\]\s*(.+?)\[END_CAPTION\]', '', text, flags=re.DOTALL)
- text = re.sub(r'\[BEGIN_TWOCOL\](.+?)\[BREAK\]\s*(.+?)\[END_TWOCOL\]', '', text, flags=re.DOTALL)
- text = re.sub(r'\[TIGHTSPACING\] *', '', text)
- text = re.sub(r'\[SINGLESPACING\] *', '', text)
- text = re.sub(r'\[DOUBLESPACING\] *', '', text)
- text = re.sub(r'\[ONEANDAHALFSPACING\] *', '', text)
- text = re.sub(r'\[TRIPLESPACING\] *', '', text)
- text = re.sub(r'\[NBSP\]', ' ', text)
- text = re.sub(r'\[REDACTION_SPACE\]', "\u200B", text)
- text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', lambda x: repeat_along('█', x), text)
- text = re.sub(r'\[ENDASH\]', '--', text)
- text = re.sub(r'\[EMDASH\]', '---', text)
- text = re.sub(r'\[HYPHEN\]', '-', text)
- text = re.sub(r'\[CHECKBOX\]', '____', text)
- text = re.sub(r'\[BLANK\]', r'__________________', text)
- text = re.sub(r'\[BLANKFILL\]', r'__________________', text)
- text = re.sub(r'\[PAGEBREAK\] *', '', text)
- text = re.sub(r'\[PAGENUM\] *', '', text)
- text = re.sub(r'\[TOTALPAGES\] *', '', text)
- text = re.sub(r'\[SECTIONNUM\] *', '', text)
- text = re.sub(r'\[SKIPLINE\] *', '\n\n', text)
- text = re.sub(r'\[VERTICALSPACE\] *', '\n\n', text)
- text = re.sub(r'\[NEWLINE\] *', '\n\n', text)
- text = re.sub(r'\[NEWPAR\] *', '\n\n', text)
- text = re.sub(r'\[BR\] *', '\n\n', text)
- text = re.sub(r'\[TAB\] *', '', text)
- text = re.sub(r' *\[END\] *', r'\n', text)
- text = re.sub(r'\[BORDER\] *(.+?)\n *\n', r'\1\n\n', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[NOINDENT\] *(.+?)\n *\n', r'\1\n\n', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[FLUSHLEFT\] *(.+?)\n *\n', r'\1\n\n', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[FLUSHRIGHT\] *(.+?)\n *\n', r'\1\n\n', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[CENTER\] *(.+?)\n *\n', r'\1\n\n', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[BOLDCENTER\] *(.+?)\n *\n', r'**\1**\n\n', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+)\] *(.+?)\n *\n', r'\2', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+) *([0-9]+ *[A-Za-z]+)\] *(.+?)\n *\n', r'\3', text, flags=re.MULTILINE | re.DOTALL)
- return text
-
-
-def docx_template_filter(text, question=None, replace_newlines=True):
- # logmessage('docx_template_filter')
- if text == 'True':
- return True
- if text == 'False':
- return False
- if text == 'None':
- return None
- text = re.sub(r'\[\[([^\]]*)\]\]', r'\1', text)
- text = re.sub(r'\[EMOJI ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_docx_template(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', lambda x: image_include_docx_template(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_docx_template(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+)\]', lambda x: image_include_docx_template(x, question=question), text)
- text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', qr_include_docx_template, text)
- text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+)\]', qr_include_docx_template, text)
- text = re.sub(r'\[QR ([^\]]+)\]', qr_include_docx_template, text)
- text = re.sub(r'\[MAP ([^\]]+)\]', '', text)
- text = replace_fields(text)
- # text = re.sub(r'\[FIELD ([^\]]+)\]', '', text)
- text = re.sub(r'\[TARGET ([^\]]+)\]', '', text)
- text = re.sub(r'\[YOUTUBE[^ ]* ([^\]]+)\]', '', text)
- text = re.sub(r'\[VIMEO[^ ]* ([^\]]+)\]', '', text)
- text = re.sub(r'\\clearpage *\\clearpage', '', text)
- text = re.sub(r'\[START_INDENTATION\]', '', text)
- text = re.sub(r'\[STOP_INDENTATION\]', '', text)
- text = re.sub(r'\[BEGIN_CAPTION\](.+?)\[VERTICAL_LINE\]\s*(.+?)\[END_CAPTION\]', '', text, flags=re.DOTALL)
- text = re.sub(r'\[BEGIN_TWOCOL\](.+?)\[BREAK\]\s*(.+?)\[END_TWOCOL\]', '', text, flags=re.DOTALL)
- text = re.sub(r'\[TIGHTSPACING\] *', '', text)
- text = re.sub(r'\[SINGLESPACING\] *', '', text)
- text = re.sub(r'\[DOUBLESPACING\] *', '', text)
- text = re.sub(r'\[ONEANDAHALFSPACING\] *', '', text)
- text = re.sub(r'\[TRIPLESPACING\] *', '', text)
- text = re.sub(r'\[NBSP\]', ' ', text)
- text = re.sub(r'\[REDACTION_SPACE\]', "\u200B", text)
- # text = re.sub(r'\[REDACTION_SPACE\]', r'', text)
- text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', lambda x: repeat_along('█', x), text)
- # text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', lambda x: repeat_along('X', x), text)
- text = re.sub(r'\[ENDASH\]', '--', text)
- text = re.sub(r'\[EMDASH\]', '---', text)
- text = re.sub(r'\[HYPHEN\]', '-', text)
- text = re.sub(r'\\', '', text)
- text = re.sub(r'\[CHECKBOX\]', '____', text)
- text = re.sub(r'\[BLANK\]', r'__________________', text)
- text = re.sub(r'\[BLANKFILL\]', r'__________________', text)
- text = re.sub(r'\[PAGEBREAK\] *', '', text)
- text = re.sub(r'\[PAGENUM\] *', '', text)
- text = re.sub(r'\[TOTALPAGES\] *', '', text)
- text = re.sub(r'\[SECTIONNUM\] *', '', text)
- text = re.sub(r'\[SKIPLINE\] *', '', text)
- text = re.sub(r'\[VERTICALSPACE\] *', ' ', text)
- text = re.sub(r'\[NEWLINE\] *', ' ', text)
- # text = re.sub(r'\n *\n', '[NEWPAR]', text)
- if replace_newlines:
- text = re.sub(r'\n', ' ', text)
- text = re.sub(r'\[NEWPAR\] *', ' ', text)
- text = re.sub(r'\[TAB\] *', '\t', text)
- text = re.sub(r'\[NEWPAR\]', ' ', text)
- text = re.sub(r' *\[END\] *', r' ', text)
- text = re.sub(r'\[BORDER\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[NOINDENT\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[FLUSHLEFT\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[FLUSHRIGHT\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[CENTER\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[BOLDCENTER\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+)\] *(.+?)\n *\n', r'\2', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+) *([0-9]+ *[A-Za-z]+)\] *(.+?)\n *\n', r'\3', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[BR\]', ' ', text)
- text = re.sub(r'\[SKIPLINE\]', ' ', text)
- text = re.sub(r'{([{%#])', '{' + zerowidth + r'\1', re.sub(r'([}%#])}', r'\1' + zerowidth + '}', text))
- return text
-
-
-def metadata_filter(text, doc_format):
- if doc_format == 'pdf':
- text = re.sub(r'\*\*([^\*]+?)\*\*', r'\\begingroup\\bfseries \1\\endgroup {}', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\*([^\*]+?)\*', r'\\begingroup\\itshape \1\\endgroup {}', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\_\_([^\_]+?)\_\_', r'\\begingroup\\bfseries \1\\endgroup {}', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\_([^\_]+?)\_*', r'\\begingroup\\itshape \1\\endgroup {}', text, flags=re.MULTILINE | re.DOTALL)
- return text
-
-
-def redact_latex(match):
- return '\\redactword{' + str(escape_latex(match.group(1))) + '}'
-
-
-def pdf_filter(text, metadata=None, question=None):
- if metadata is None:
- metadata = {}
- text = text + "\n\n"
- text = re.sub(r'\[\[([^\]]*)\]\]', r'\1', text)
- text = re.sub(r'\[EMOJI ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_string(x, emoji=True, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', lambda x: image_include_string(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_string(x, question=question), text)
- text = re.sub(r'\[FILE ([^,\]]+)\]', lambda x: image_include_string(x, question=question), text)
- text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', qr_include_string, text)
- text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+)\]', qr_include_string, text)
- text = re.sub(r'\[QR ([^\]]+)\]', qr_include_string, text)
- text = re.sub(r'\[MAP ([^\]]+)\]', '', text)
- text = replace_fields(text)
- # text = re.sub(r'\[FIELD ([^\]]+)\]', '', text)
- text = re.sub(r'\[TARGET ([^\]]+)\]', '', text)
- text = re.sub(r'\[YOUTUBE[^ ]* ([^\]]+)\]', '', text)
- text = re.sub(r'\[VIMEO[^ ]* ([^\]]+)\]', '', text)
- text = re.sub(r'\$\$+', '$', text)
- text = re.sub(r'\\clearpage *\\clearpage', r'\\clearpage', text)
- text = re.sub(r'\[BORDER\]\s*\[(BEGIN_TWOCOL|BEGIN_CAPTION|TIGHTSPACING|SINGLESPACING|DOUBLESPACING|START_INDENTATION|STOP_INDENTATION|NOINDENT|FLUSHLEFT|FLUSHRIGHT|CENTER|BOLDCENTER|INDENTBY[^\]]*)\]', r'[\1] [BORDER]', text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[START_INDENTATION\]', r'\\setlength{\\parindent}{\\myindentamount}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
- text = re.sub(r'\[STOP_INDENTATION\]', r'\\setlength{\\parindent}{0in}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
- text = re.sub(r'\[BEGIN_CAPTION\](.+?)\[VERTICAL_LINE\]\s*(.+?)\[END_CAPTION\]', pdf_caption, text, flags=re.DOTALL)
- text = re.sub(r'\[BEGIN_TWOCOL\](.+?)\[BREAK\]\s*(.+?)\[END_TWOCOL\]', pdf_two_col, text, flags=re.DOTALL)
- text = re.sub(r'\[TIGHTSPACING\]\s*', r'\\singlespacing\\setlength{\\parskip}{0pt}\\setlength{\\parindent}{0pt}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
- text = re.sub(r'\[SINGLESPACING\]\s*', r'\\singlespacing\\setlength{\\parskip}{\\myfontsize}\\setlength{\\parindent}{0pt}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
- text = re.sub(r'\[DOUBLESPACING\]\s*', r'\\doublespacing\\setlength{\\parindent}{\\myindentamount}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
- text = re.sub(r'\[ONEANDAHALFSPACING\]\s*', r'\\onehalfspacing\\setlength{\\parindent}{\\myindentamount}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
- text = re.sub(r'\[TRIPLESPACING\]\s*', r'\\setlength{\\parindent}{\\myindentamount}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
- text = re.sub(r'\[NBSP\]', r'\\myshow{\\nonbreakingspace}', text)
- text = re.sub(r'\[REDACTION_SPACE\]', r'\\redactword{~}\\hspace{0pt}', text)
- text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', redact_latex, text)
- text = re.sub(r'\[ENDASH\]', r'\\myshow{\\myendash}', text)
- text = re.sub(r'\[EMDASH\]', r'\\myshow{\\myemdash}', text)
- text = re.sub(r'\[HYPHEN\]', r'\\myshow{\\myhyphen}', text)
- text = re.sub(r'\[CHECKBOX\]', r'{\\rule{0.3in}{0.4pt}}', text)
- text = re.sub(r'\[BLANK\]', r'\\leavevmode{\\xrfill[-2pt]{0.4pt}}', text)
- text = re.sub(r'\[BLANKFILL\]', r'\\leavevmode{\\xrfill[-2pt]{0.4pt}}', text)
- text = re.sub(r'\[PAGEBREAK\]\s*', r'\\clearpage ', text)
- text = re.sub(r'\[PAGENUM\]', r'\\myshow{\\thepage\\myxspace}', text)
- text = re.sub(r'\[TOTALPAGES\]', r'\\myshow{\\pageref*{LastPage}\\myxspace}', text)
- text = re.sub(r'\[SECTIONNUM\]', r'\\myshow{\\thesection\\myxspace}', text)
- text = re.sub(r'\[VERTICALSPACE\] *', r'\\rule[-24pt]{0pt}{0pt}', text)
- text = re.sub(r'\[NEWLINE\] *', r'\\newline ', text)
- text = re.sub(r'\[NEWPAR\] *', r'\\par ', text)
- text = re.sub(r'\[BR\] *', r'\\manuallinebreak ', text)
- text = re.sub(r'\[TAB\] *', r'\\manualindent ', text)
- text = re.sub(r' *\[END\] *', r'\n', text)
- text = re.sub(r'\[NOINDENT\] *', r'\\noindent ', text)
- text = re.sub(r'\[FLUSHLEFT\] *(.+?)\n *\n', flushleft_pdf, text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[FLUSHRIGHT\] *(.+?)\n *\n', flushright_pdf, text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[CENTER\] *(.+?)\n *\n', center_pdf, text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[BOLDCENTER\] *(.+?)\n *\n', boldcenter_pdf, text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+)\] *(.+?)\n *\n', indentby_left_pdf, text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+) *([0-9]+ *[A-Za-z]+)\] *(.+?)\n *\n', indentby_both_pdf, text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\[BORDER\] *(.+?)\n *\n', border_pdf, text, flags=re.MULTILINE | re.DOTALL)
- text = re.sub(r'\s*\[SKIPLINE\]\s*', r'\\par\\myskipline ', text)
- return text
-
-
-def html_filter(text, status=None, question=None, embedder=None, default_image_width=None, external=False):
- if question is None and status is not None:
- question = status.question
- text = text + "\n\n"
- text = re.sub(r'^[|] (.*)$', r'\1
', text, flags=re.MULTILINE)
- text = replace_fields(text, status=status, embedder=embedder)
- # if embedder is not None:
- # text = re.sub(r'\[FIELD ([^\]]+)\]', lambda x: embedder(status, x.group(1)), text)
- # else:
- # text = re.sub(r'\[FIELD ([^\]]+)\]', 'ERROR: FIELD cannot be used here', text)
- text = re.sub(r'\[TARGET ([^\]]+)\]', target_html, text)
- if docassemble.base.functions.this_thread.evaluation_context != 'docx':
- text = re.sub(r'\[EMOJI ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_url_string(x, emoji=True, question=question, external=external, status=status), text)
- text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', lambda x: image_url_string(x, question=question, external=external, status=status), text)
- text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_url_string(x, question=question, external=external, status=status), text)
- text = re.sub(r'\[FILE ([^,\]]+)\]', lambda x: image_url_string(x, question=question, default_image_width=default_image_width, external=external, status=status), text)
- text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', qr_url_string, text)
- text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+)\]', qr_url_string, text)
- text = re.sub(r'\[QR ([^,\]]+)\]', qr_url_string, text)
- if map_match.search(text):
- text = map_match.sub((lambda x: map_string(x.group(1), status)), text)
- # width="420" height="315"
- text = re.sub(r'\[YOUTUBE ([^\]]+)\]', r'', text)
- text = re.sub(r'\[YOUTUBE4:3 ([^\]]+)\]', r'', text)
- text = re.sub(r'\[YOUTUBE16:9 ([^\]]+)\]', r'', text)
- # width="500" height="281"
- text = re.sub(r'\[VIMEO ([^\]]+)\]', r'', text)
- text = re.sub(r'\[VIMEO4:3 ([^\]]+)\]', r'', text)
- text = re.sub(r'\[VIMEO16:9 ([^\]]+)\]', r'', text)
- text = re.sub(r'\[BEGIN_CAPTION\](.+?)\[VERTICAL_LINE\]\s*(.+?)\[END_CAPTION\]', html_caption, text, flags=re.DOTALL)
- text = re.sub(r'\[BEGIN_TWOCOL\](.+?)\[BREAK\]\s*(.+?)\[END_TWOCOL\]', html_two_col, text, flags=re.DOTALL)
- text = re.sub(r'\[NBSP\]', r' ', text)
- text = re.sub(r'\[REDACTION_SPACE\]', '█', text)
- text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', lambda x: repeat_along('█', x), text)
- text = re.sub(r'\[ENDASH\]', r'–', text)
- text = re.sub(r'\[EMDASH\]', r'—', text)
- text = re.sub(r'\[HYPHEN\]', r'-', text)
- text = re.sub(r'\[CHECKBOX\]', r' ', text)
- text = re.sub(r'\[BLANK\]', r' ', text)
- text = re.sub(r'\[BLANKFILL\]', r' ', text)
- text = re.sub(r'\[PAGEBREAK\] *', r'', text)
- text = re.sub(r'\[PAGENUM\] *', r'', text)
- text = re.sub(r'\[SECTIONNUM\] *', r'', text)
- text = re.sub(r'\[SKIPLINE\] *', r'
', text)
- text = re.sub(r'\[NEWLINE\] *', r'
', text)
- text = re.sub(r'\[NEWPAR\] *', r'
', text)
- text = re.sub(r'\[BR\] *', r'
', text)
- text = re.sub(r'\[TAB\] *', '', text)
- text = re.sub(r' *\[END\] *', r'\n', text)
- lines = re.split(r'\n *\n', text)
- text = ''
- spacing_class = None
- doing_indentation = False
- for line in lines:
- classes = set()
- styles = {}
- if re.search(r'\[TIGHTSPACING\]', line):
- spacing_class = 'daspacingtight'
- if re.search(r'\[SINGLESPACING\]', line):
- spacing_class = 'daspacingsingle'
- if re.search(r'\[DOUBLESPACING\]', line):
- spacing_class = 'daspacingdouble'
- if re.search(r'\[ONEANDAHALFSPACING\]', line):
- spacing_class = 'daspacingoneandahalf'
- if re.search(r'\[TRIPLESPACING\]', line):
- spacing_class = 'daspacingtriple'
- if re.search(r'\[START_INDENTATION\]', line):
- doing_indentation = True
- if re.search(r'\[STOP_INDENTATION\]', line):
- doing_indentation = False
- if spacing_class:
- classes.add(spacing_class)
- if doing_indentation and not re.search(r'\[NOINDENT\]', line):
- styles['text-indent'] = '36px'
- if re.search(r'\[BORDER\]', line):
- classes.add('daborder')
- if re.search(r'\[FLUSHLEFT\]', line):
- classes.add('daflushleft')
- if re.search(r'\[FLUSHRIGHT\]', line):
- classes.add('daflushright')
- if re.search(r'\[CENTER\]', line):
- classes.add('dacenter')
- if re.search(r'\[BOLDCENTER\]', line):
- classes.add('dacenter')
- classes.add('dabold')
- m = re.search(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+)\]', line)
- if m:
- styles["padding-left"] = str(convert_length(m.group(1), 'px')) + 'px'
- m = re.search(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+) *([0-9]+ *[A-Za-z]+)\]', line)
- if m:
- styles["margin-left"] = str(convert_length(m.group(1), 'px')) + 'px'
- styles["margin-right"] = str(convert_length(m.group(2), 'px')) + 'px'
- orig_length = len(line)
- line = re.sub(r'\[(BORDER|NOINDENT|FLUSHLEFT|FLUSHRIGHT|BOLDCENTER|CENTER|TIGHTSPACING|SINGLESPACING|DOUBLESPACING|ONEANDAHALFSPACING|TRIPLESPACING|START_INDENTATION|STOP_INDENTATION)\] *', r'', line)
- line = re.sub(r'\[INDENTBY[^\]]*\] *', r'', line)
- if orig_length > 0 and len(line) == 0:
- continue
- if line.startswith('>'):
- line = re.sub(r'^> *', '', line)
- text += "> "
- if len(classes) > 0 or len(styles) > 0:
- text += ' 0:
- text += ' style="' + "".join(map(lambda x: str(x[0]) + ":" + x[1] + ';', styles.items())) + '"'
- text += '>'
- text += line + '\n\n'
- text = re.sub(r'\n+$', r'', text)
- return text
-
-
-def clean_markdown_to_latex(string):
- string = re.sub(r'\s*\[SKIPLINE\]\s*', r'\\par\\myskipline ', string)
- string = re.sub(r'^[\n ]+', '', string)
- string = re.sub(r'[\n ]+$', '', string)
- string = re.sub(r' *\n *$', '\n', string)
- string = re.sub(r'\n{2,}', '[NEWLINE]', string)
- string = re.sub(r'\[BR\]', '[NEWLINE]', string)
- string = re.sub(r'\[(NOINDENT|FLUSHLEFT|FLUSHRIGHT|CENTER|BOLDCENTER|TIGHTSPACING|SINGLESPACING|DOUBLESPACING|START_INDENTATION|STOP_INDENTATION|PAGEBREAK)\]\s*', '', string)
- string = re.sub(r'\*\*([^\*]+?)\*\*', r'\\textbf{\1}', string)
- string = re.sub(r'\*([^\*]+?)\*', r'\\emph{\1}', string)
- string = re.sub(r'(?'
-
-
-def target_html(match):
- target = match.group(1)
- target = re.sub(r'[^A-Za-z0-9\_]', r'', str(target))
- return ''
-
-
-def pdf_two_col(match, add_line=False):
- firstcol = clean_markdown_to_latex(match.group(1))
- secondcol = clean_markdown_to_latex(match.group(2))
- if add_line:
- return '\\noindent\\begingroup\\singlespacing\\setlength{\\parskip}{0pt}\\mynoindent\\begin{tabular}{@{}m{0.49\\textwidth}|@{\\hspace{1em}}m{0.49\\textwidth}@{}}{' + firstcol + '} & {' + secondcol + '} \\\\ \\end{tabular}\\endgroup\\myskipline'
- return '\\noindent\\begingroup\\singlespacing\\setlength{\\parskip}{0pt}\\mynoindent\\begin{tabular}{@{}m{0.49\\textwidth}@{\\hspace{1em}}m{0.49\\textwidth}@{}}{' + firstcol + '} & {' + secondcol + '} \\\\ \\end{tabular}\\endgroup\\myskipline'
-
-
-def html_caption(match):
- firstcol = match.group(1)
- secondcol = match.group(2)
- firstcol = re.sub(r'^\s+', '', firstcol)
- firstcol = re.sub(r'\s+$', '', firstcol)
- secondcol = re.sub(r'^\s+', '', secondcol)
- secondcol = re.sub(r'\s+$', '', secondcol)
- firstcol = markdown_to_html(firstcol)
- secondcol = markdown_to_html(secondcol)
- return '' + firstcol + ' ' + secondcol + '
'
-
-
-def html_two_col(match):
- firstcol = markdown_to_html(match.group(1))
- secondcol = markdown_to_html(match.group(2))
- return '' + firstcol + ' ' + secondcol + '
'
-
-
-def pdf_caption(match):
- return pdf_two_col(match, add_line=False)
-
-
-def add_newlines(string):
- string = re.sub(r'\[(BR)\]', r'[NEWLINE]', string)
- string = re.sub(r' *\n', r'\n', string)
- string = re.sub(r'(? 0 and file_info['width'] > 0:
- scale = float(pixels)/float(file_info['width'])
- # logmessage("scale is " + str(scale))
- if scale*float(file_info['height']) > float(MAX_HEIGHT_POINTS):
- scale = float(MAX_HEIGHT_POINTS)/float(file_info['height'])
- # logmessage("scale is " + str(scale))
- if scale*float(file_info['width']) > float(MAX_WIDTH_POINTS):
- scale = float(MAX_WIDTH_POINTS)/float(file_info['width'])
- # logmessage("scale is " + str(scale))
- # scale *= 100.0
- # logmessage("scale is " + str(scale))
- # scale = int(scale)
- # logmessage("scale is " + str(scale))
- wtwips = int(scale*float(file_info['width'])*20.0)
- htwips = int(scale*float(file_info['height'])*20.0)
- image = Image(file_info['fullpath'])
- image.Data = re.sub(r'\\picwgoal([0-9]+)', r'\\picwgoal' + str(wtwips), image.Data)
- image.Data = re.sub(r'\\pichgoal([0-9]+)', r'\\pichgoal' + str(htwips), image.Data)
- else:
- image = Image(file_info['fullpath'])
- if insert_page_breaks:
- content = '\\page '
- else:
- content = ''
- # logmessage(content + image.Data)
- return content + image.Data
-
-unit_multipliers = {'twips': 0.0500, 'hp': 0.5, 'in': 72, 'pt': 1, 'px': 1, 'em': 12, 'cm': 28.346472}
-
-
-def convert_length(length, unit):
- value = pixels_in(length)
- if unit in unit_multipliers:
- size = float(value)/float(unit_multipliers[unit])
- return int(size)
- logmessage("Unit " + str(unit) + " is not a valid unit")
- return 300
-
-
-def pixels_in(length):
- m = re.search(r"([0-9.]+) *([a-z]+)", str(length).lower())
- if m:
- value = float(m.group(1))
- unit = m.group(2)
- # logmessage("value is " + str(value) + " and unit is " + unit)
- if unit in unit_multipliers:
- size = float(unit_multipliers[unit]) * value
- # logmessage("size is " + str(size))
- return int(size)
- logmessage("Could not read " + str(length))
- return 300
-
-
-def image_url_string(match, emoji=False, question=None, default_image_width=None, external=False, status=None):
- file_reference = match.group(1)
- try:
- width = match.group(2)
- assert width != 'None'
- except:
- if default_image_width is not None:
- width = default_image_width
- else:
- width = "300px"
- if width == "full":
- width = "300px"
- if match.lastindex == 3:
- if match.group(3) != 'None':
- alt_text = 'alt=' + json.dumps(match.group(3)) + ' '
- else:
- alt_text = ''
- else:
- alt_text = ''
- return image_url(file_reference, alt_text, width, emoji=emoji, question=question, external=external, status=status)
-
-
-def image_url(file_reference, alt_text, width, emoji=False, question=None, external=False, status=None):
- if question and file_reference in question.interview.images:
- if status and question.interview.images[file_reference].attribution is not None:
- status.attributions.add(question.interview.images[file_reference].attribution)
- file_reference = question.interview.images[file_reference].get_reference()
- file_info = server.file_finder(file_reference, question=question)
- if 'mimetype' in file_info and file_info['mimetype']:
- if re.search(r'^audio', file_info['mimetype']):
- urls = get_audio_urls([{'text': "[FILE " + file_reference + "]", 'package': None, 'type': 'audio'}], question=question)
- if len(urls) > 0:
- return audio_control(urls)
- return ''
- if re.search(r'^video', file_info['mimetype']):
- urls = get_video_urls([{'text': "[FILE " + file_reference + "]", 'package': None, 'type': 'video'}], question=question)
- if len(urls) > 0:
- return video_control(urls)
- return ''
- if 'extension' in file_info and file_info['extension'] is not None:
- if re.match(r'.*%$', width):
- width_string = "width:" + width
- stack_width_string = width_string
- else:
- width_string = "max-width:" + width
- stack_width_string = "width:" + width
- if emoji:
- width_string += ';vertical-align: middle'
- alt_text = 'alt="" '
- the_url = server.url_finder(file_reference, _question=question, display_filename=file_info['filename'], _external=external)
- if the_url is None:
- return '[ERROR: File reference ' + str(file_reference) + ' cannot be displayed]'
- if width_string == 'width:100%':
- extra_class = ' dawideimage'
- else:
- extra_class = ''
- if file_info.get('extension', '') in ('png', 'jpg', 'gif', 'svg', 'jpe', 'jpeg'):
- try:
- if file_info.get('extension', '') == 'svg':
- attributes = ET.parse(file_info['fullpath']).getroot().attrib
- layout_width = attributes['width']
- layout_height = attributes['height']
- else:
- with PIL.Image.open(file_info['fullpath']) as im:
- layout_width, layout_height = im.size
- return '
'
- except:
- return '
'
- if file_info['extension'] in ('pdf', 'docx', 'rtf', 'doc', 'odt'):
- if file_info['extension'] in ('docx', 'rtf', 'doc', 'odt') and not os.path.isfile(file_info['path'] + '.pdf'):
- server.fg_make_pdf_for_word_path(file_info['path'], file_info['extension'])
- server.fg_make_png_for_pdf_path(file_info['path'] + ".pdf", 'screen', page=1)
- if re.match(r'[0-9]+', str(file_reference)):
- sf = server.SavedFile(int(file_reference), fix=True)
- sf.finalize()
- if 'pages' not in file_info:
- try:
- with Pdf.open(file_info['path'] + '.pdf') as reader:
- file_info['pages'] = len(reader.pages)
- except:
- file_info['pages'] = 1
- the_image_url = server.url_finder(file_reference, size="screen", page=1, _question=question, _external=external)
- if the_image_url is None:
- return '[ERROR: File reference ' + str(file_reference) + ' cannot be displayed]'
- if 'filename' in file_info:
- title = ' title="' + file_info['filename']
- if 'pages' in file_info and file_info['pages'] > 1:
- title += " (" + str(file_info['pages']) + " " + word('pages') + ")"
- title += '"'
- else:
- if 'pages' in file_info and file_info['pages'] > 1:
- title = ' title="' + str(file_info['pages']) + " " + word('pages') + '"'
- else:
- title = ''
- if alt_text == '':
- the_alt_text = 'alt=' + json.dumps(word("Thumbnail image of document")) + ' '
- else:
- the_alt_text = alt_text
- try:
- with Pdf.open(file_info['path'] + '.pdf') as reader:
- layout_width = reader.pages[0].mediabox[2] - reader.pages[0].mediabox[0]
- layout_height = reader.pages[0].mediabox[3] - reader.pages[0].mediabox[1]
- if width_string == 'width:100%':
- output = '
'
- else:
- if 'pages' in file_info and file_info['pages'] >= 1:
- extra_pages = min(2, file_info['pages'] - 1)
- else:
- extra_pages = 2
- aspect_ratio = 1.0*layout_width/layout_height
- stack_width_string += "; height: auto; aspect-ratio: " + str(aspect_ratio) + ";"
- output = ''
- except:
- output = '
'
- return output
- return '' + file_info['filename'] + ''
- return '[Invalid image reference; reference=' + str(file_reference) + ', width=' + str(width) + ', filename=' + file_info.get('filename', 'unknown') + ']'
-
-
-def qr_url_string(match):
- string = match.group(1)
- try:
- width = match.group(2)
- assert width != 'None'
- except:
- width = "300px"
- if width == "full":
- width = "300px"
- if match.lastindex == 3:
- if match.group(3) != 'None':
- alt_text = str(match.group(3))
- else:
- alt_text = word(f"A QR code that goes to {string}")
- else:
- alt_text = word(f"A QR code that goes to {string}")
- width_string = "width:" + width
- im = qrcode.make(string, image_factory=qrcode.image.svg.SvgPathFillImage)
- output = BytesIO()
- im.save(output)
- the_image = output.getvalue().decode()
- the_image = re.sub(r"<\?xml version='1.0' encoding='UTF-8'\?>\n", '', the_image)
- the_image = re.sub(r'height="[0-9]+mm" ', '', the_image)
- the_image = re.sub(r'width="[0-9]+mm" ', '', the_image)
- m = re.search(r'(viewBox="[^"]+")', the_image)
- if m:
- viewbox = m.group(1)
- else:
- viewbox = ""
- return ''
-
-
-def convert_pixels(match):
- pixels = match.group(1)
- return str(int(pixels)/72.0) + "in"
-
-
-def convert_percent(match):
- percentage = match.group(1)
- return str(float(percentage)/100.0) + '\\textwidth'
-
-
-def image_include_string(match, emoji=False, question=None):
- file_reference = match.group(1)
- if question and file_reference in question.interview.images:
- file_reference = question.interview.images[file_reference].get_reference()
- try:
- width = match.group(2)
- assert width != 'None'
- width = re.sub(r'^(.*)px', convert_pixels, width)
- width = re.sub(r'^(.*)%', convert_percent, width)
- if width == "full":
- width = '\\textwidth'
- except:
- width = DEFAULT_IMAGE_WIDTH
- if match.lastindex == 3:
- alt_text = match.group(3)
- else:
- alt_text = None
- file_info = server.file_finder(file_reference, question=question)
- if 'path' in file_info and 'extension' in file_info:
- convert_svg_to_eps(file_info)
- if file_info['extension'] == 'gif':
- with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".png", delete=False) as png_file:
- try:
- with PIL.Image.open(file_info['fullpath']) as im:
- im.save(png_file.name)
- png_file.close()
- file_info['path'] = png_file.name
- file_info['fullpath'] = png_file.name
- file_info['extension'] = 'png'
- file_info['mimetype'] = 'image/png'
- except BaseException as err:
- logmessage("Could not convert GIF to PNG: " + err.__class__.__name__ + ": " + str(err))
- if 'mimetype' in file_info and file_info['mimetype']:
- if re.search(r'^(audio|video)', file_info['mimetype']):
- return '[reference to file type that cannot be displayed]'
- if 'path' in file_info:
- if 'extension' in file_info:
- if file_info['extension'] in ['png', 'jpg', 'pdf', 'eps', 'jpe', 'jpeg', 'docx', 'rtf', 'doc', 'odt']:
- if file_info['extension'] == 'pdf':
- output = '\\includepdf[pages={-}]{' + file_info['path'] + '.pdf}'
- elif file_info['extension'] in ('docx', 'rtf', 'doc', 'odt'):
- if not os.path.isfile(file_info['path'] + '.pdf'):
- server.fg_make_pdf_for_word_path(file_info['path'], file_info['extension'])
- output = '\\includepdf[pages={-}]{' + file_info['path'] + '.pdf}'
- else:
- if alt_text:
- alt_text_string = ', alt={' + re.sub(r'[{}]', '', alt_text) + '}'
- else:
- alt_text_string = ''
- if emoji:
- output = '\\raisebox{-.6\\dp\\strutbox}{\\mbox{\\includegraphics[width=' + width + alt_text_string + ']{' + file_info['path'] + '}}}'
- else:
- output = '\\mbox{\\includegraphics[width=' + width + alt_text_string + ']{' + file_info['path'] + '}}'
- if width == '\\textwidth':
- output = '\\clearpage ' + output + '\\clearpage '
- return output
- return '[invalid graphics reference]'
-
-
-def image_include_docx(match, question=None):
- file_reference = match.group(1)
- if question and file_reference in question.interview.images:
- file_reference = question.interview.images[file_reference].get_reference()
- try:
- width = match.group(2)
- assert width != 'None'
- width = re.sub(r'^(.*)px', convert_pixels, width)
- if width == "full":
- width = '100%'
- except:
- width = DEFAULT_IMAGE_WIDTH
- if match.lastindex == 3:
- alt_text = match.group(3)
- else:
- alt_text = None
- if not alt_text:
- alt_text = ''
- file_info = server.file_finder(file_reference, question=question)
- if 'mimetype' in file_info and file_info['mimetype']:
- if re.search(r'^(audio|video)', file_info['mimetype']):
- return '[reference to file type that cannot be displayed]'
- if 'path' in file_info:
- if 'extension' in file_info:
- convert_svg_to_eps(file_info)
- if file_info['extension'] in ('docx', 'rtf', 'doc', 'odt'):
- if not os.path.isfile(file_info['path'] + '.pdf'):
- server.fg_make_pdf_for_word_path(file_info['path'], file_info['extension'])
- output = '{width=' + width + '}'
- return output
- if file_info['extension'] in ['png', 'jpg', 'gif', 'pdf', 'eps', 'jpe', 'jpeg']:
- output = '{width=' + width + '}'
- return output
- return '[invalid graphics reference]'
-
-
-def qr_include_string(match):
- string = match.group(1)
- try:
- width = match.group(2)
- assert width != 'None'
- width = re.sub(r'^(.*)px', convert_pixels, width)
- if width == "full":
- width = '\\textwidth'
- except:
- width = DEFAULT_IMAGE_WIDTH
- if match.lastindex == 3:
- alt_text = match.group(3)
- else:
- alt_text = None
- im = qrcode.make(string)
- with tempfile.NamedTemporaryFile(prefix="datemp", suffix=".png", delete=False) as the_image:
- # docassemble.base.functions.this_thread.temporary_resources.add(the_image.name)
- im.save(the_image.name)
- if alt_text:
- alt_text_string = ', alt={' + re.sub(r'[{}]', '', alt_text) + '}'
- else:
- alt_text_string = ''
- output = '\\mbox{\\includegraphics[width=' + width + alt_text_string + ']{' + the_image.name + '}}'
- if width == '\\textwidth':
- output = '\\clearpage ' + output + '\\clearpage '
- # logmessage("Output is " + output)
- return output
-
-
-def qr_include_docx(match):
- string = match.group(1)
- try:
- width = match.group(2)
- assert width != 'None'
- width = re.sub(r'^(.*)px', convert_pixels, width)
- if width == "full":
- width = '100%'
- except:
- width = DEFAULT_IMAGE_WIDTH
- if match.lastindex == 3:
- alt_text = match.group(3)
- else:
- alt_text = None
- if not alt_text:
- alt_text = ''
- im = qrcode.make(string)
- with tempfile.NamedTemporaryFile(prefix="datemp", suffix=".png", delete=False) as the_image:
- # docassemble.base.functions.this_thread.temporary_resources.add(the_image.name)
- im.save(the_image.name)
- output = '{width=' + width + '}'
- return output
-
-
-def rtf_caption_table(match):
- table_text = """\\trowd \\irow0\\irowband0\\lastrow \\ltrrow\\ts24\\trgaph108\\trleft0\\trbrdrt\\brdrs\\brdrw10 \\trbrdrl\\brdrs\\brdrw10 \\trbrdrb\\brdrs\\brdrw10 \\trbrdrr\\brdrs\\brdrw10 \\trbrdrh\\brdrs\\brdrw10 \\trbrdrv\\brdrs\\brdrw10
-\\trftsWidth1\\trftsWidthB3\\trftsWidthA3\\trautofit1\\trpaddl108\\trpaddr108\\trpaddfl3\\trpaddft3\\trpaddfb3\\trpaddfr3\\trcbpat1\\trcfpat1\\tblrsid1508006\\tbllkhdrrows\\tbllkhdrcols\\tbllknocolband\\tblind0\\tblindtype3 \\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone
-\\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrs\\brdrw10 \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx4680\\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrs\\brdrw10 \\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx9468\\pard\\plain \\ltrpar
-\\ql \\li0\\ri0\\widctlpar\\intbl\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\pararsid1508006\\yts24 \\rtlch\\fcs1 \\af0\\afs22\\alang1025 \\ltrch\\fcs0 \\fs22\\lang1033\\langfe1033\\cgrid\\langnp1033\\langfenp1033 { [SAVE][TIGHTSPACING][STOP_INDENTATION]""" + match.group(1) + """}{\\cell}{""" + match.group(2) + """[RESTORE]}{\\cell}\\pard\\plain \\ltrpar
-\\ql \\li0\\ri0\\sa200\\sl276\\slmult1\\widctlpar\\intbl\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0 \\rtlch\\fcs1 \\af0\\afs22\\alang1025 \\ltrch\\fcs0 \\fs24\\lang1033\\langfe1033\\cgrid\\langnp1033\\langfenp1033 {\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\insrsid10753242
-\\trowd \\irow0\\irowband0\\lastrow \\ltrrow\\ts24\\trgaph108\\trleft0\\trbrdrt\\brdrs\\brdrw10 \\trbrdrl\\brdrs\\brdrw10 \\trbrdrb\\brdrs\\brdrw10 \\trbrdrr\\brdrs\\brdrw10 \\trbrdrh\\brdrs\\brdrw10 \\trbrdrv\\brdrs\\brdrw10
-\\trftsWidth1\\trftsWidthB3\\trftsWidthA3\\trautofit1\\trpaddl108\\trpaddr108\\trpaddfl3\\trpaddft3\\trpaddfb3\\trpaddfr3\\trcbpat1\\trcfpat1\\tblrsid1508006\\tbllkhdrrows\\tbllkhdrcols\\tbllknocolband\\tblind0\\tblindtype3 \\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone
-\\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrs\\brdrw10 \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx4680\\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrs\\brdrw10 \\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx9468\\row }"""
- table_text += """\\pard \\ltrpar
-\\qc \\li0\\ri0\\sb0\\sl240\\slmult1\\widctlpar\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\itap0\\pararsid10753242"""
- table_text = re.sub(r'\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0', r'\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\sl240 \\slmult1', table_text)
- return table_text + '[MANUALSKIP]'
-
-
-def rtf_two_col(match):
- table_text = """\\trowd \\irow0\\irowband0\\lastrow \\ltrrow\\ts24\\trgaph108\\trleft0\\trbrdrt\\brdrs\\brdrw10 \\trbrdrl\\brdrs\\brdrw10 \\trbrdrb\\brdrs\\brdrw10 \\trbrdrr\\brdrs\\brdrw10 \\trbrdrh\\brdrs\\brdrw10 \\trbrdrv\\brdrs\\brdrw10
-\\trftsWidth1\\trftsWidthB3\\trftsWidthA3\\trautofit1\\trpaddl108\\trpaddr108\\trpaddfl3\\trpaddft3\\trpaddfb3\\trpaddfr3\\trcbpat1\\trcfpat1\\tblrsid1508006\\tbllkhdrrows\\tbllkhdrcols\\tbllknocolband\\tblind0\\tblindtype3 \\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone
-\\clbrdrb\\brdrnone \\clbrdrr\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx4680\\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone \\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx9468\\pard\\plain \\ltrpar
-\\ql \\li0\\ri0\\widctlpar\\intbl\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\pararsid1508006\\yts24 \\rtlch\\fcs1 \\af0\\afs22\\alang1025 \\ltrch\\fcs0 \\fs22\\lang1033\\langfe1033\\cgrid\\langnp1033\\langfenp1033 {\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\insrsid2427490 [SAVE][TIGHTSPACING][STOP_INDENTATION]""" + match.group(1) + """}{\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\insrsid10753242\\charrsid2427490 \\cell}{""" + match.group(2) + """[RESTORE]}{\\cell}\\pard\\plain \\ltrpar
-\\ql \\li0\\ri0\\sa200\\sl276\\slmult1\\widctlpar\\intbl\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0 \\rtlch\\fcs1 \\af0\\afs22\\alang1025 \\ltrch\\fcs0 \\fs24\\lang1033\\langfe1033\\cgrid\\langnp1033\\langfenp1033 {\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\insrsid10753242
-\\trowd \\irow0\\irowband0\\lastrow \\ltrrow\\ts24\\trgaph108\\trleft0\\trbrdrt\\brdrs\\brdrw10 \\trbrdrl\\brdrs\\brdrw10 \\trbrdrb\\brdrs\\brdrw10 \\trbrdrr\\brdrs\\brdrw10 \\trbrdrh\\brdrs\\brdrw10 \\trbrdrv\\brdrs\\brdrw10
-\\trftsWidth1\\trftsWidthB3\\trftsWidthA3\\trautofit1\\trpaddl108\\trpaddr108\\trpaddfl3\\trpaddft3\\trpaddfb3\\trpaddfr3\\trcbpat1\\trcfpat1\\tblrsid1508006\\tbllkhdrrows\\tbllkhdrcols\\tbllknocolband\\tblind0\\tblindtype3 \\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone
-\\clbrdrb\\brdrnone \\clbrdrr\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx4680\\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone \\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx9468\\row }"""
- table_text += """\\pard \\ltrpar
-\\qc \\li0\\ri0\\sb0\\sl240\\slmult1\\widctlpar\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\itap0\\pararsid10753242"""
- table_text = re.sub(r'\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0', r'\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\sl240 \\slmult1', table_text)
- return table_text + '[MANUALSKIP]'
-
-
-def get_icon_html(text):
- icons_setting = docassemble.base.functions.get_config('default icons', None)
- if icons_setting == 'font awesome':
- m = re.search(r'^(fa[a-z])-fa-(.*)', text)
- if m:
- the_prefix = m.group(1)
- text = m.group(2)
- else:
- the_prefix = docassemble.base.functions.get_config('font awesome prefix', 'fa-solid')
- if the_prefix == 'fab':
- the_prefix = 'fa-brands'
- elif the_prefix == 'far':
- the_prefix = 'fa-regular'
- elif the_prefix == 'fas':
- the_prefix = 'fa-solid'
- return ''
- if icons_setting == 'material icons':
- return '' + str(text) + ''
- return None
-
-
-def emoji_html(text, status=None, question=None, images=None):
- # logmessage("Got to emoji_html")
- if status is not None and question is None:
- question = status.question
- if images is None:
- images = question.interview.images
- if text in images:
- if status is not None and images[text].attribution is not None:
- status.attributions.add(images[text].attribution)
- return image_url(images[text].get_reference(), word('icon'), '1em', emoji=True, question=question)
- icon_html = get_icon_html(text)
- if icon_html:
- return icon_html
- return ":" + str(text) + ":"
-
-
-def emoji_insert(text, status=None, images=None):
- if images is None:
- images = status.question.interview.images
- if text in images:
- if status is not None and images[text].attribution is not None:
- status.attributions.add(images[text].attribution)
- return "[EMOJI " + images[text].get_reference() + ', 1.2em]'
- return ":" + str(text) + ":"
-
-
-def link_rewriter(m, status):
- the_path = None
- if m.group(1).startswith('#'):
- return ' 0:
- lang = docassemble.base.functions.get_language()
- for term in question.terms:
- terms_done.add(term.lower())
- # logmessage("Searching for term " + term + " in " + a)
- if lang in question.terms[term]['re']:
- a = question.terms[term]['re'][lang].sub(sub_term, a)
- else:
- a = question.terms[term]['re'][question.language].sub(sub_term, a)
- # logmessage("string is now " + str(a))
- if len(question.autoterms) > 0:
- lang = docassemble.base.functions.get_language()
- for term in question.autoterms:
- if term.lower() in terms_done:
- continue
- terms_done.add(term.lower())
- # logmessage("Searching for term " + term + " in " + a)
- if lang in question.autoterms[term]['re']:
- a = question.autoterms[term]['re'][lang].sub(r'[[\1]]', a)
- else:
- a = question.autoterms[term]['re'][question.language].sub(r'[[\1]]', a)
- # logmessage("string is now " + str(a))
- if 'interview_terms' in status.extras:
- interview_terms = status.extras['interview_terms']
- else:
- interview_terms = question.interview.terms
- if 'interview_autoterms' in status.extras:
- interview_autoterms = status.extras['interview_autoterms']
- else:
- interview_autoterms = question.interview.autoterms
- else:
- interview_terms = question.interview.terms
- interview_autoterms = question.interview.autoterms
- if len(interview_terms) > 0:
- lang = docassemble.base.functions.get_language()
- if lang in interview_terms and len(interview_terms[lang]) > 0:
- for term in interview_terms[lang]:
- if term.lower() in terms_done:
- continue
- terms_done.add(term.lower())
- # logmessage("Searching for term " + term + " in " + a)
- a = interview_terms[lang][term]['re'].sub(sub_term, a)
- # logmessage("string is now " + str(a))
- elif question.language in interview_terms and len(interview_terms[question.language]) > 0:
- for term in interview_terms[question.language]:
- if term.lower() in terms_done:
- continue
- terms_done.add(term.lower())
- # logmessage("Searching for term " + term + " in " + a)
- a = interview_terms[question.language][term]['re'].sub(sub_term, a)
- # logmessage("string is now " + str(a))
- if len(interview_autoterms) > 0:
- lang = docassemble.base.functions.get_language()
- if lang in interview_autoterms and len(interview_autoterms[lang]) > 0:
- for term in interview_autoterms[lang]:
- if term.lower() in terms_done:
- continue
- terms_done.add(term.lower())
- # logmessage("Searching for term " + term + " in " + a)
- a = interview_autoterms[lang][term]['re'].sub(r'[[\1]]', a)
- # logmessage("string is now " + str(a))
- elif question.language in interview_autoterms and len(interview_autoterms[question.language]) > 0:
- for term in interview_autoterms[question.language]:
- if term.lower() in terms_done:
- continue
- terms_done.add(term.lower())
- # logmessage("Searching for term " + term + " in " + a)
- a = interview_autoterms[question.language][term]['re'].sub(r'[[\1]]', a)
- # logmessage("string is now " + str(a))
- a = html_filter(str(a), status=status, question=question, embedder=embedder, default_image_width=default_image_width, external=external)
- # logmessage("before: " + a)
- if status and status.extras.get('tableCssClass', None):
- classes = status.extras['tableCssClass'].split(',')
- table_class = json.dumps(classes[0].strip())
- if len(classes) > 1:
- thead_class = json.dumps(classes[1].strip())
- else:
- thead_class = None
- else:
- table_class = server.default_table_class
- thead_class = server.default_thead_class
- a = re.sub(r'<(/?)table', r'<\1TABLE', a)
- a = re.sub(r'', r'', a)
- if use_pandoc:
- converter = pandoc.MyPandoc()
- converter.output_format = 'html'
- converter.input_content = a
- converter.convert(question)
- result = converter.output_content
- else:
- try:
- result = docassemble.base.functions.this_thread.markdown.reset().convert(a)
- except:
- # Try again because sometimes it fails randomly and maybe trying again will work.
- result = docassemble.base.functions.this_thread.markdown.reset().convert(a)
- result = re.sub(r'', r'', result)
- if thead_class:
- result = re.sub(r'', r'', result)
- result = re.sub(r'
', r'
', result)
- result = re.sub(r'<(/?)TABLE', r'<\1table', result)
- result = re.sub(r'', r'', result)
- result = re.sub(r'<(t[dh]) align="(right|left|center)">', r'<\1 class="text-\2">', result)
- result = re.sub(r'', r'', result)
- result = re.sub(r' 0 and 'terms' in status.extras:
- result = term_match.sub((lambda x: add_terms(x.group(1), status.extras['terms'], label=x.group(2), status=status, question=question)), result)
- if len(question.autoterms) > 0 and 'autoterms' in status.extras:
- result = term_match.sub((lambda x: add_terms(x.group(1), status.extras['autoterms'], label=x.group(2), status=status, question=question)), result)
- if 'interview_terms' in status.extras:
- interview_terms = status.extras['interview_terms']
- else:
- interview_terms = question.interview.terms
- if 'interview_autoterms' in status.extras:
- interview_autoterms = status.extras['interview_autoterms']
- else:
- interview_autoterms = question.interview.autoterms
- else:
- interview_terms = question.interview.terms
- interview_autoterms = question.interview.autoterms
- if lang in interview_terms and len(interview_terms[lang]):
- result = term_match.sub((lambda x: add_terms(x.group(1), interview_terms[lang], label=x.group(2), status=status, question=question)), result)
- elif question.language in interview_terms and len(interview_terms[question.language]):
- result = term_match.sub((lambda x: add_terms(x.group(1), interview_terms[question.language], label=x.group(2), status=status, question=question)), result)
- if lang in interview_autoterms and len(interview_autoterms[lang]):
- result = term_match.sub((lambda x: add_terms(x.group(1), interview_autoterms[lang], label=x.group(2), status=status, question=question)), result)
- elif question.language in interview_autoterms and len(interview_autoterms[question.language]):
- result = term_match.sub((lambda x: add_terms(x.group(1), interview_autoterms[question.language], label=x.group(2), status=status, question=question)), result)
- do_not_scan_for_emojis = bool(re.search(r'\[NO_EMOJIS\]', result))
- if do_not_scan_for_emojis:
- result = re.sub(r'\[NO_EMOJIS\]\s*', r'', result)
- if status is not None and question.interview.scan_for_emojis and not do_not_scan_for_emojis:
- result = emoji_match.sub((lambda x: emoji_html(x.group(1), status=status, question=question)), result)
- result = re.sub(r'', result)
- if trim:
- if result.startswith('
') and result.endswith('
'):
- result = re.sub(r'\s*', ' ', result[3:-4])
- elif pclass:
- result = re.sub('
', '
', result)
- if escape:
- if escape is True:
- result = noquote_match.sub('"', result)
- if escape == 'option':
- result = re.sub(r'\n\r', ' ', BeautifulSoup(result, 'html.parser').get_text()).strip()
- result = lt_match.sub('<', result)
- result = gt_match.sub('>', result)
- if escape is True:
- result = amp_match.sub('&', result)
- # logmessage("after: " + result)
- # result = result.replace('\n', ' ')
- if result:
- if strip_newlines:
- result = result.replace('\n', ' ')
- if divclass is not None:
- result = '
' + result + ''
- # if indent and not code_match.search(result):
- # return (" " * indent) + re.sub(r'\n', "\n" + (" " * indent), result).rstrip() + "\n"
- return result
-
-
-def my_escape(result):
- result = noquote_match.sub('"', result)
- result = lt_match.sub('<', result)
- result = gt_match.sub('>', result)
- result = amp_match.sub('&', result)
- return result
-
-
-def noquote(string):
- # return json.dumps(string.replace('\n', ' ').rstrip())
- return '"' + string.replace('\n', ' ').replace('"', '"').rstrip() + '"'
-
-
-def add_terms_mako(termname, terms, status=None, question=None):
- lower_termname = re.sub(r'\s+', ' ', str(termname).lower(), re.DOTALL)
- if lower_termname in terms:
- term_as_text = to_text(markdown_to_html(str(termname), trim=False, do_terms=False, status=status, question=question), None, None)
- return '' + str(termname) + ''
- # logmessage(lower_termname + " is not in terms dictionary")
- return '[[' + termname + ']]'
-
-
-def add_terms(termname, terms, label=None, status=None, question=None):
- if label is None:
- label = str(termname)
- else:
- label = re.sub(r'^\|', '', label)
- lower_termname = re.sub(r'\s+', ' ', termname.lower(), re.DOTALL)
- if lower_termname in terms:
- term_as_text = to_text(markdown_to_html(label, trim=False, do_terms=False, status=status, question=question), None, None)
- return '' + label + ''
- return '[[' + termname + ']]'
-
-
-def audio_control(files, preload="metadata", title_text=None):
- for d in files:
- if isinstance(d, str):
- return d
- if title_text is None:
- title_text = ''
- else:
- title_text = " title=" + json.dumps(title_text)
- output = '\n"
- return output
-
-
-def video_control(files):
- for d in files:
- if isinstance(d, (str, NoneType)):
- return str(d)
- output = '\n"
- return output
-
-
-def get_audio_urls(the_audio, question=None):
- output = []
- the_list = []
- to_try = {}
- for audio_item in the_audio:
- if audio_item['type'] != 'audio':
- continue
- found_upload = False
- pattern = re.compile(r'^\[FILE ([^,\]]+)')
- for file_ref in re.findall(pattern, audio_item['text']):
- found_upload = True
- m = re.match(r'[0-9]+', file_ref)
- if m:
- file_info = server.file_finder(file_ref, question=question)
- if 'path' in file_info:
- if file_info['mimetype'] == 'audio/ogg':
- output.append([server.url_finder(file_ref, _question=question), file_info['mimetype']])
- elif os.path.isfile(file_info['path'] + '.ogg'):
- output.append([server.url_finder(file_ref, ext='ogg', _question=question), 'audio/ogg'])
- if file_info['mimetype'] == 'audio/mpeg':
- output.append([server.url_finder(file_ref, _question=question), file_info['mimetype']])
- elif os.path.isfile(file_info['path'] + '.mp3'):
- output.append([server.url_finder(file_ref, ext='mp3', _question=question), 'audio/mpeg'])
- if file_info['mimetype'] not in ['audio/mpeg', 'audio/ogg']:
- output.append([server.url_finder(file_ref, _question=question), file_info['mimetype']])
- else:
- the_list.append({'text': file_ref, 'package': audio_item['package']})
- if not found_upload:
- the_list.append(audio_item)
- for audio_item in the_list:
- mimetype, encoding = mimetypes.guess_type(audio_item['text']) # pylint: disable=unused-variable
- if re.search(r'^http', audio_item['text']):
- output.append([audio_item['text'], mimetype])
- continue
- basename = os.path.splitext(audio_item['text'])[0]
- ext = os.path.splitext(audio_item['text'])[1]
- if mimetype not in to_try:
- to_try[mimetype] = []
- to_try[mimetype].append({'basename': basename, 'filename': audio_item['text'], 'ext': ext, 'package': audio_item['package']})
- if 'audio/mpeg' in to_try and 'audio/ogg' not in to_try:
- to_try['audio/ogg'] = []
- for attempt in to_try['audio/mpeg']:
- if attempt['ext'] == '.MP3':
- to_try['audio/ogg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.OGG', 'ext': '.OGG', 'package': attempt['package']})
- else:
- to_try['audio/ogg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.ogg', 'ext': '.ogg', 'package': attempt['package']})
- if 'audio/ogg' in to_try and 'audio/mpeg' not in to_try:
- to_try['audio/mpeg'] = []
- for attempt in to_try['audio/ogg']:
- if attempt['ext'] == '.OGG':
- to_try['audio/mpeg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.MP3', 'ext': '.MP3', 'package': attempt['package']})
- else:
- to_try['audio/mpeg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.mp3', 'ext': '.mp3', 'package': attempt['package']})
- for mimetype in reversed(sorted(to_try.keys())):
- for attempt in to_try[mimetype]:
- parts = attempt['filename'].split(':')
- if len(parts) < 2:
- parts = [attempt['package'], attempt['filename']]
- if parts[0] is None:
- parts[0] = 'None'
- parts[1] = re.sub(r'^data/static/', '', parts[1])
- full_file = parts[0] + ':data/static/' + parts[1]
- file_info = server.file_finder(full_file, question=question)
- if 'fullpath' in file_info:
- url = server.url_finder(full_file, _question=question)
- output.append([url, mimetype])
- return [item for item in output if item[0] is not None]
-
-
-def get_video_urls(the_video, question=None):
- output = []
- the_list = []
- to_try = {}
- for video_item in the_video:
- if video_item['type'] != 'video':
- continue
- found_upload = False
- if re.search(r'^\[(YOUTUBE|VIMEO)[0-9\:]* ', video_item['text']):
- output.append(html_filter(video_item['text']))
- continue
- pattern = re.compile(r'^\[FILE ([^,\]]+)')
- for file_ref in re.findall(pattern, video_item['text']):
- found_upload = True
- m = re.match(r'[0-9]+', file_ref)
- if m:
- file_info = server.file_finder(file_ref, question=question)
- if 'path' in file_info:
- if file_info['mimetype'] == 'video/ogg':
- output.append([server.url_finder(file_ref, _question=question), file_info['mimetype']])
- elif os.path.isfile(file_info['path'] + '.ogv'):
- output.append([server.url_finder(file_ref, ext='ogv', _question=question), 'video/ogg'])
- if file_info['mimetype'] == 'video/mp4':
- output.append([server.url_finder(file_ref, _question=question), file_info['mimetype']])
- elif os.path.isfile(file_info['path'] + '.mp4'):
- output.append([server.url_finder(file_ref, ext='mp4', _question=question), 'video/mp4'])
- if file_info['mimetype'] not in ['video/mp4', 'video/ogg']:
- output.append([server.url_finder(file_ref, _question=question), file_info['mimetype']])
- else:
- the_list.append({'text': file_ref, 'package': video_item['package']})
- if not found_upload:
- the_list.append(video_item)
- for video_item in the_list:
- mimetype, encoding = mimetypes.guess_type(video_item['text']) # pylint: disable=unused-variable
- if re.search(r'^http', video_item['text']):
- output.append([video_item['text'], mimetype])
- continue
- basename = os.path.splitext(video_item['text'])[0]
- ext = os.path.splitext(video_item['text'])[1]
- if mimetype not in to_try:
- to_try[mimetype] = []
- to_try[mimetype].append({'basename': basename, 'filename': video_item['text'], 'ext': ext, 'package': video_item['package']})
- if 'video/mp4' in to_try and 'video/ogg' not in to_try:
- to_try['video/ogg'] = []
- for attempt in to_try['video/mp4']:
- if attempt['ext'] == '.MP4':
- to_try['video/ogg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.OGV', 'ext': '.OGV', 'package': attempt['package']})
- else:
- to_try['video/ogg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.ogv', 'ext': '.ogv', 'package': attempt['package']})
- if 'video/ogg' in to_try and 'video/mp4' not in to_try:
- to_try['video/mp4'] = []
- for attempt in to_try['video/ogg']:
- if attempt['ext'] == '.OGV':
- to_try['video/mp4'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.MP4', 'ext': '.MP4', 'package': attempt['package']})
- else:
- to_try['audio/mpeg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.mp4', 'ext': '.mp4', 'package': attempt['package']})
- for mimetype in reversed(sorted(to_try.keys())):
- for attempt in to_try[mimetype]:
- parts = attempt['filename'].split(':')
- if len(parts) < 2:
- parts = [attempt['package'], attempt['filename']]
- parts[1] = re.sub(r'^data/static/', '', parts[1])
- if parts[0] is None:
- full_file = 'data/static/' + parts[1]
- else:
- full_file = parts[0] + ':data/static/' + parts[1]
- file_info = server.file_finder(full_file, question=question)
- if 'fullpath' in file_info:
- url = server.url_finder(full_file, _question=question)
- if url is not None:
- output.append([url, mimetype])
- return output
-
-
-def process_target(text):
- return re.sub(r'\[TARGET ([^\]]+)\]', target_html, text)
-
-
-def to_text(html_doc, terms, links):
- output = ""
- # logmessage("to_text: html doc is " + str(html_doc))
- if not html_doc.startswith('<'):
- html_doc = "" + html_doc + ""
- soup = BeautifulSoup(html_doc, 'html.parser')
- [s.extract() for s in soup(['style', 'script', '[document]', 'head', 'title', 'audio', 'video', 'pre', 'attribution'])] # pylint: disable=expression-not-assigned
- [s.extract() for s in soup.find_all(hidden)] # pylint: disable=expression-not-assigned
- [s.extract() for s in soup.find_all('div', {'class': 'dainvisible'})] # pylint: disable=expression-not-assigned
- for s in soup.find_all(do_show):
- if s.name in ['input', 'textarea', 'img'] and s.has_attr('alt'):
- words = s.attrs['alt']
- if s.has_attr('placeholder'):
- words += ", " + s.attrs['placeholder']
- else:
- words = s.get_text()
- words = re.sub(r'\n\s*', ' ', words, flags=re.DOTALL)
- output += words + "\n"
- for s in soup.find_all('a'):
- if s.has_attr('class') and s.attrs['class'][0] == 'daterm' and s.has_attr('data-bs-content'):
- terms[s.string] = s.attrs['data-bs-content']
- elif s.has_attr('href'): # and (s.attrs['href'].startswith(url) or s.attrs['href'].startswith('?')):
- # logmessage("Adding a link: " + s.attrs['href'])
- links.append((s.attrs['href'], s.get_text()))
- output = re.sub(br'\u201c'.decode('raw_unicode_escape'), '"', output)
- output = re.sub(br'\u201d'.decode('raw_unicode_escape'), '"', output)
- output = re.sub(br'\u2018'.decode('raw_unicode_escape'), "'", output)
- output = re.sub(br'\u2019'.decode('raw_unicode_escape'), "'", output)
- output = re.sub(br'\u201b'.decode('raw_unicode_escape'), "'", output)
- output = re.sub(r'>', '>', output)
- output = re.sub(r'<', '<', output)
- output = re.sub(r'>', '>', output)
- output = re.sub(r'<', '<', output)
- output = re.sub(r'<[^>]+>', '', output)
- output = re.sub(r'\n$', '', output)
- output = re.sub(r' +', ' ', output)
- return output
-
-bad_list = ['div', 'option']
-
-good_list = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'button', 'textarea', 'note']
-
-
-def do_show(element):
- if re.match('', str(element), re.DOTALL):
- return False
- if element.name in ['option'] and element.has_attr('selected'):
- return True
- if element.name in bad_list:
- return False
- if element.name in ['img', 'input'] and element.has_attr('alt'):
- return True
- if element.name in good_list:
- return True
- if element.parent and element.parent.name in good_list:
- return False
- if element.string:
- return True
- if re.match(r'\s+', element.get_text()):
- return False
- return False
-
-
-def hidden(element):
- if element.name == 'input':
- if element.has_attr('type'):
- if element.attrs['type'] == 'hidden':
- return True
- return False
-
-
-def replace_fields(string, status=None, embedder=None):
- if not re.search(r'\[FIELD ', string):
- return string
- matches = []
- in_match = False
- start_match = None
- depth = 0
- i = 0
- while i < len(string):
- if string[i:i+7] == '[FIELD ':
- in_match = True
- start_match = i
- i += 7
- continue
- if in_match:
- if string[i] == '[':
- depth += 1
- elif string[i] == ']':
- if depth == 0:
- i += 1
- matches.append((start_match, i))
- in_match = False
- continue
- depth -= 1
- i += 1
-
- field_strings = []
- for (start, end) in matches:
- field_strings.append(string[start:end])
- # logmessage(repr(field_strings))
- for field_string in field_strings:
- if embedder is None:
- string = string.replace(field_string, 'ERROR: FIELD cannot be used here')
- else:
- string = string.replace(field_string, embedder(status, field_string))
- return string
-
-
-def image_include_docx_template(match, question=None):
- file_reference = match.group(1)
- if question and file_reference in question.interview.images:
- file_reference = question.interview.images[file_reference].get_reference()
- try:
- width = match.group(2)
- assert width != 'None'
- width = re.sub(r'^(.*)px', convert_pixels, width)
- if width == "full":
- width = '100%'
- except:
- width = DEFAULT_IMAGE_WIDTH
- if match.lastindex == 3:
- alt_text = match.group(3)
- else:
- alt_text = None
- file_info = server.file_finder(file_reference, question=question)
- if 'mimetype' in file_info and file_info['mimetype']:
- if re.search(r'^(audio|video)', file_info['mimetype']):
- return '[reference to file type that cannot be displayed]'
- if 'path' in file_info:
- convert_svg_to_eps(file_info)
- if 'mimetype' in file_info and file_info['mimetype']:
- if file_info['mimetype'] in ('text/markdown', 'text/plain'):
- with open(file_info['fullpath'], 'r', encoding='utf-8') as f:
- contents = f.read()
- if file_info['mimetype'] == 'text/plain':
- return contents
- return docassemble.base.file_docx.markdown_to_docx(contents, question, docassemble.base.functions.this_thread.misc.get('docx_template', None))
- if file_info['mimetype'] == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
- return str(docassemble.base.file_docx.include_docx_template(docassemble.base.functions.DALocalFile(file_info['fullpath'])))
- return str(docassemble.base.file_docx.image_for_docx(file_reference, question, docassemble.base.functions.this_thread.misc.get('docx_template', None), width=width, alt_text=alt_text))
- return '[reference to file that could not be found]'
-
-
-def qr_include_docx_template(match):
- string = match.group(1)
- try:
- width = match.group(2)
- assert width != 'None'
- width = re.sub(r'^(.*)px', convert_pixels, width)
- if width == "full":
- width = '100%'
- except:
- width = DEFAULT_IMAGE_WIDTH
- if match.lastindex == 3:
- alt_text = match.group(3)
- else:
- alt_text = None
- im = qrcode.make(string)
- with tempfile.NamedTemporaryFile(prefix="datemp", suffix=".png", delete=False) as the_image:
- im.save(the_image.name)
- return str(docassemble.base.file_docx.image_for_docx(docassemble.base.functions.DALocalFile(the_image.name), None, docassemble.base.functions.this_thread.misc.get('docx_template', None), width=width, alt_text=alt_text))
-
-
-def convert_svg_to_eps(file_info):
- try:
- if file_info['extension'] == 'svg':
- with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".eps", delete=False) as eps_file:
- with open(file_info['fullpath'], 'rb') as fp:
- svg2eps(file_obj=fp, write_to=eps_file)
- file_info['path'] = eps_file.name
- file_info['fullpath'] = eps_file.name
- file_info['extension'] = 'eps'
- file_info['mimetype'] = 'application/postscript'
- eps_file.close()
- except BaseException as err:
- logmessage("Failure to convert SVG to EPS: " + err.__class__.__name__ + ": " + str(err))
-
-
-def convert_svg_to_png(file_info):
- try:
- if file_info['extension'] == 'svg':
- with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".png", delete=False) as png_file:
- with open(file_info['fullpath'], 'rb') as fp:
- svg2png(file_obj=fp, write_to=png_file, dpi=300)
- png_file.flush()
- with PIL.Image.open(png_file.name) as im:
- file_info['width'], file_info['height'] = im.size
- file_info['path'] = png_file.name
- file_info['fullpath'] = png_file.name
- file_info['extension'] = 'png'
- file_info['mimetype'] = 'image/png'
- png_file.close()
- except BaseException as err:
- logmessage("Failure to convert SVG to PNG: " + err.__class__.__name__ + ": " + str(err))
diff --git a/docassemble_webapp/docassemble/webapp/templates/pages/__init__.py b/docassemble_base/docassemble/base/filter/__init__.py
similarity index 100%
rename from docassemble_webapp/docassemble/webapp/templates/pages/__init__.py
rename to docassemble_base/docassemble/base/filter/__init__.py
diff --git a/docassemble_base/docassemble/base/filter/docx.py b/docassemble_base/docassemble/base/filter/docx.py
new file mode 100644
index 000000000..0fef46c66
--- /dev/null
+++ b/docassemble_base/docassemble/base/filter/docx.py
@@ -0,0 +1,803 @@
+import re
+import os
+import codecs
+from copy import deepcopy
+import tempfile
+import string
+from bs4 import BeautifulSoup, NavigableString, Tag
+import docx
+import qrcode
+import qrcode.image.svg
+from docxtpl import RichText
+from ..config import daconfig
+from ..functions import DALocalFile, roman, package_template_filename
+from ..hooks import file_finder, fg_make_pdf_for_word_path
+from ..logger import logmessage
+from ..thread_context import this_thread
+from .docx_subdoc import fix_subdoc
+from .html import markdown_to_html
+from .image_docx import image_for_docx
+from .utils import (
+ repeat_along,
+ replace_fields,
+ convert_pixels,
+ get_default_image_width,
+ convert_svg_to_eps,
+ list_types,
+ zerowidth,
+ sanitize_xml,
+)
+
+NoneType = type(None)
+
+def docx_filter(text, metadata=None, question=None):
+ if metadata is None:
+ metadata = {}
+ text = text + "\n\n"
+ text = re.sub(r'\[\[([^\]]*)\]\]', r'\1', text)
+ text = re.sub(r'\[EMOJI ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_docx(x, question=question), text)
+ text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', lambda x: image_include_docx(x, question=question), text)
+ text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_docx(x, question=question), text)
+ text = re.sub(r'\[FILE ([^,\]]+)\]', lambda x: image_include_docx(x, question=question), text)
+ text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', qr_include_docx, text)
+ text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+)\]', qr_include_docx, text)
+ text = re.sub(r'\[QR ([^\]]+)\]', qr_include_docx, text)
+ text = re.sub(r'\[MAP ([^\]]+)\]', '', text)
+ text = replace_fields(text)
+ # text = re.sub(r'\[FIELD ([^\]]+)\]', '', text)
+ text = re.sub(r'\[TARGET ([^\]]+)\]', '', text)
+ text = re.sub(r'\[YOUTUBE[^ ]* ([^\]]+)\]', '', text)
+ text = re.sub(r'\[VIMEO[^ ]* ([^\]]+)\]', '', text)
+ text = re.sub(r'\\clearpage *\\clearpage', '', text)
+ text = re.sub(r'\[START_INDENTATION\]', '', text)
+ text = re.sub(r'\[STOP_INDENTATION\]', '', text)
+ text = re.sub(r'\[BEGIN_CAPTION\](.+?)\[VERTICAL_LINE\]\s*(.+?)\[END_CAPTION\]', '', text, flags=re.DOTALL)
+ text = re.sub(r'\[BEGIN_TWOCOL\](.+?)\[BREAK\]\s*(.+?)\[END_TWOCOL\]', '', text, flags=re.DOTALL)
+ text = re.sub(r'\[TIGHTSPACING\] *', '', text)
+ text = re.sub(r'\[SINGLESPACING\] *', '', text)
+ text = re.sub(r'\[DOUBLESPACING\] *', '', text)
+ text = re.sub(r'\[ONEANDAHALFSPACING\] *', '', text)
+ text = re.sub(r'\[TRIPLESPACING\] *', '', text)
+ text = re.sub(r'\[NBSP\]', ' ', text)
+ text = re.sub(r'\[REDACTION_SPACE\]', "\u200B", text)
+ text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', lambda x: repeat_along('█', x), text)
+ text = re.sub(r'\[ENDASH\]', '--', text)
+ text = re.sub(r'\[EMDASH\]', '---', text)
+ text = re.sub(r'\[HYPHEN\]', '-', text)
+ text = re.sub(r'\[CHECKBOX\]', '____', text)
+ text = re.sub(r'\[BLANK\]', r'__________________', text)
+ text = re.sub(r'\[BLANKFILL\]', r'__________________', text)
+ text = re.sub(r'\[PAGEBREAK\] *', '', text)
+ text = re.sub(r'\[PAGENUM\] *', '', text)
+ text = re.sub(r'\[TOTALPAGES\] *', '', text)
+ text = re.sub(r'\[SECTIONNUM\] *', '', text)
+ text = re.sub(r'\[SKIPLINE\] *', '\n\n', text)
+ text = re.sub(r'\[VERTICALSPACE\] *', '\n\n', text)
+ text = re.sub(r'\[NEWLINE\] *', '\n\n', text)
+ text = re.sub(r'\[NEWPAR\] *', '\n\n', text)
+ text = re.sub(r'\[BR\] *', '\n\n', text)
+ text = re.sub(r'\[TAB\] *', '', text)
+ text = re.sub(r' *\[END\] *', r'\n', text)
+ text = re.sub(r'\[BORDER\] *(.+?)\n *\n', r'\1\n\n', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[NOINDENT\] *(.+?)\n *\n', r'\1\n\n', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[FLUSHLEFT\] *(.+?)\n *\n', r'\1\n\n', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[FLUSHRIGHT\] *(.+?)\n *\n', r'\1\n\n', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[CENTER\] *(.+?)\n *\n', r'\1\n\n', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[BOLDCENTER\] *(.+?)\n *\n', r'**\1**\n\n', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+)\] *(.+?)\n *\n', r'\2', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+) *([0-9]+ *[A-Za-z]+)\] *(.+?)\n *\n', r'\3', text, flags=re.MULTILINE | re.DOTALL)
+ return text
+
+
+def docx_template_filter(text, question=None, replace_newlines=True):
+ # logmessage('docx_template_filter')
+ if text == 'True':
+ return True
+ if text == 'False':
+ return False
+ if text == 'None':
+ return None
+ text = re.sub(r'\[\[([^\]]*)\]\]', r'\1', text)
+ text = re.sub(r'\[EMOJI ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_docx_template(x, question=question), text)
+ text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', lambda x: image_include_docx_template(x, question=question), text)
+ text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_docx_template(x, question=question), text)
+ text = re.sub(r'\[FILE ([^,\]]+)\]', lambda x: image_include_docx_template(x, question=question), text)
+ text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', qr_include_docx_template, text)
+ text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+)\]', qr_include_docx_template, text)
+ text = re.sub(r'\[QR ([^\]]+)\]', qr_include_docx_template, text)
+ text = re.sub(r'\[MAP ([^\]]+)\]', '', text)
+ text = replace_fields(text)
+ # text = re.sub(r'\[FIELD ([^\]]+)\]', '', text)
+ text = re.sub(r'\[TARGET ([^\]]+)\]', '', text)
+ text = re.sub(r'\[YOUTUBE[^ ]* ([^\]]+)\]', '', text)
+ text = re.sub(r'\[VIMEO[^ ]* ([^\]]+)\]', '', text)
+ text = re.sub(r'\\clearpage *\\clearpage', '', text)
+ text = re.sub(r'\[START_INDENTATION\]', '', text)
+ text = re.sub(r'\[STOP_INDENTATION\]', '', text)
+ text = re.sub(r'\[BEGIN_CAPTION\](.+?)\[VERTICAL_LINE\]\s*(.+?)\[END_CAPTION\]', '', text, flags=re.DOTALL)
+ text = re.sub(r'\[BEGIN_TWOCOL\](.+?)\[BREAK\]\s*(.+?)\[END_TWOCOL\]', '', text, flags=re.DOTALL)
+ text = re.sub(r'\[TIGHTSPACING\] *', '', text)
+ text = re.sub(r'\[SINGLESPACING\] *', '', text)
+ text = re.sub(r'\[DOUBLESPACING\] *', '', text)
+ text = re.sub(r'\[ONEANDAHALFSPACING\] *', '', text)
+ text = re.sub(r'\[TRIPLESPACING\] *', '', text)
+ text = re.sub(r'\[NBSP\]', ' ', text)
+ text = re.sub(r'\[REDACTION_SPACE\]', "\u200B", text)
+ # text = re.sub(r'\[REDACTION_SPACE\]', r'', text)
+ text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', lambda x: repeat_along('█', x), text)
+ # text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', lambda x: repeat_along('X', x), text)
+ text = re.sub(r'\[ENDASH\]', '--', text)
+ text = re.sub(r'\[EMDASH\]', '---', text)
+ text = re.sub(r'\[HYPHEN\]', '-', text)
+ text = re.sub(r'\\', '', text)
+ text = re.sub(r'\[CHECKBOX\]', '____', text)
+ text = re.sub(r'\[BLANK\]', r'__________________', text)
+ text = re.sub(r'\[BLANKFILL\]', r'__________________', text)
+ text = re.sub(r'\[PAGEBREAK\] *', '', text)
+ text = re.sub(r'\[PAGENUM\] *', '', text)
+ text = re.sub(r'\[TOTALPAGES\] *', '', text)
+ text = re.sub(r'\[SECTIONNUM\] *', '', text)
+ text = re.sub(r'\[SKIPLINE\] *', '', text)
+ text = re.sub(r'\[VERTICALSPACE\] *', ' ', text)
+ text = re.sub(r'\[NEWLINE\] *', ' ', text)
+ # text = re.sub(r'\n *\n', '[NEWPAR]', text)
+ if replace_newlines:
+ text = re.sub(r'\n', ' ', text)
+ text = re.sub(r'\[NEWPAR\] *', ' ', text)
+ text = re.sub(r'\[TAB\] *', '\t', text)
+ text = re.sub(r'\[NEWPAR\]', ' ', text)
+ text = re.sub(r' *\[END\] *', r' ', text)
+ text = re.sub(r'\[BORDER\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[NOINDENT\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[FLUSHLEFT\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[FLUSHRIGHT\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[CENTER\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[BOLDCENTER\] *', r'', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+)\] *(.+?)\n *\n', r'\2', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+) *([0-9]+ *[A-Za-z]+)\] *(.+?)\n *\n', r'\3', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[BR\]', ' ', text)
+ text = re.sub(r'\[SKIPLINE\]', ' ', text)
+ text = re.sub(r'{([{%#])', '{' + zerowidth + r'\1', re.sub(r'([}%#])}', r'\1' + zerowidth + '}', text))
+ return text
+
+
+def image_include_docx(match, question=None):
+ file_reference = match.group(1)
+ if question and file_reference in question.interview.images:
+ file_reference = question.interview.images[file_reference].get_reference()
+ try:
+ width = match.group(2)
+ assert width != 'None'
+ width = re.sub(r'^(.*)px', convert_pixels, width)
+ if width == "full":
+ width = '100%'
+ except:
+ width = get_default_image_width()
+ if match.lastindex == 3:
+ alt_text = match.group(3)
+ else:
+ alt_text = None
+ if not alt_text:
+ alt_text = ''
+ file_info = file_finder(file_reference, question=question)
+ if 'mimetype' in file_info and file_info['mimetype']:
+ if re.search(r'^(audio|video)', file_info['mimetype']):
+ return '[reference to file type that cannot be displayed]'
+ if 'path' in file_info:
+ if 'extension' in file_info:
+ convert_svg_to_eps(file_info)
+ if file_info['extension'] in ('docx', 'rtf', 'doc', 'odt'):
+ if not os.path.isfile(file_info['path'] + '.pdf'):
+ fg_make_pdf_for_word_path(file_info['path'], file_info['extension'])
+ output = '{width=' + width + '}'
+ return output
+ if file_info['extension'] in ['png', 'jpg', 'gif', 'pdf', 'eps', 'jpe', 'jpeg']:
+ output = '{width=' + width + '}'
+ return output
+ return '[invalid graphics reference]'
+
+
+def image_include_docx_template(match, question=None):
+ file_reference = match.group(1)
+ if question and file_reference in question.interview.images:
+ file_reference = question.interview.images[file_reference].get_reference()
+ try:
+ width = match.group(2)
+ assert width != 'None'
+ width = re.sub(r'^(.*)px', convert_pixels, width)
+ if width == "full":
+ width = '100%'
+ except:
+ width = get_default_image_width()
+ if match.lastindex == 3:
+ alt_text = match.group(3)
+ else:
+ alt_text = None
+ file_info = file_finder(file_reference, question=question)
+ if 'mimetype' in file_info and file_info['mimetype']:
+ if re.search(r'^(audio|video)', file_info['mimetype']):
+ return '[reference to file type that cannot be displayed]'
+ if 'path' in file_info:
+ convert_svg_to_eps(file_info)
+ if 'mimetype' in file_info and file_info['mimetype']:
+ if file_info['mimetype'] in ('text/markdown', 'text/plain'):
+ with open(file_info['fullpath'], 'r', encoding='utf-8') as f:
+ contents = f.read()
+ if file_info['mimetype'] == 'text/plain':
+ return contents
+ return markdown_to_docx(contents, question, this_thread.misc.get('docx_template', None))
+ if file_info['mimetype'] == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
+ return str(include_docx_template(DALocalFile(file_info['fullpath'])))
+ return str(image_for_docx(file_reference, question, this_thread.misc.get('docx_template', None), width=width, alt_text=alt_text))
+ return '[reference to file that could not be found]'
+
+
+def qr_include_docx(match):
+ the_string = match.group(1)
+ try:
+ width = match.group(2)
+ assert width != 'None'
+ width = re.sub(r'^(.*)px', convert_pixels, width)
+ if width == "full":
+ width = '100%'
+ except:
+ width = get_default_image_width()
+ if match.lastindex == 3:
+ alt_text = match.group(3)
+ else:
+ alt_text = None
+ if not alt_text:
+ alt_text = ''
+ im = qrcode.make(the_string)
+ with tempfile.NamedTemporaryFile(prefix="datemp", suffix=".png", delete=False) as the_image:
+ # this_thread.temporary_resources.add(the_image.name)
+ im.save(the_image.name)
+ output = '{width=' + width + '}'
+ return output
+
+
+def qr_include_docx_template(match):
+ the_string = match.group(1)
+ try:
+ width = match.group(2)
+ assert width != 'None'
+ width = re.sub(r'^(.*)px', convert_pixels, width)
+ if width == "full":
+ width = '100%'
+ except:
+ width = get_default_image_width()
+ if match.lastindex == 3:
+ alt_text = match.group(3)
+ else:
+ alt_text = None
+ im = qrcode.make(the_string)
+ with tempfile.NamedTemporaryFile(prefix="datemp", suffix=".png", delete=False) as the_image:
+ im.save(the_image.name)
+ return str(image_for_docx(DALocalFile(the_image.name), None, this_thread.misc.get('docx_template', None), width=width, alt_text=alt_text))
+
+
+def Alpha(number): # pylint: disable=invalid-name
+ multiplier = int((number - 1) / 26)
+ indexno = (number - 1) % 26
+ return string.ascii_uppercase[indexno] * (multiplier + 1)
+
+
+def alpha(number):
+ multiplier = int((number - 1) / 26)
+ indexno = (number - 1) % 26
+ return string.ascii_lowercase[indexno] * (multiplier + 1)
+
+
+def Roman_Numeral(number): # pylint: disable=invalid-name
+ return roman((number - 1) % 4000, case='upper')
+
+
+def roman_numeral(number):
+ return roman((number - 1) % 4000, case='lower')
+
+
+class SoupParser:
+
+ def __init__(self, tpl):
+ self.paragraphs = [{'params': {'style': 'p', 'indentation': 0, 'list_number': 1}, 'runs': [RichText('')]}]
+ self.current_paragraph = self.paragraphs[-1]
+ self.run = self.current_paragraph['runs'][-1]
+ self.bold = False
+ self.center = False
+ self.list_number = 1
+ self.list_type = list_types[-1]
+ self.italic = False
+ self.underline = False
+ self.strike = False
+ self.indentation = 0
+ self.style = 'p'
+ self.still_new = True
+ self.size = None
+ self.charstyle = None
+ self.color = None
+ self.tpl = tpl
+
+ def new_paragraph(self, classes, styles):
+ if self.still_new:
+ # logmessage("new_paragraph is still new and style is " + self.style + " and indentation is " + str(self.indentation))
+ self.current_paragraph['params']['style'] = self.style
+ self.current_paragraph['params']['indentation'] = self.indentation
+ self.set_attribs(classes, styles)
+ self.list_number += 1
+ return
+ # logmessage("new_paragraph where style is " + self.style + " and indentation is " + str(self.indentation))
+ self.current_paragraph = {'params': {'style': self.style, 'indentation': self.indentation, 'list_number': self.list_number}, 'runs': [RichText('')]}
+ self.set_attribs(classes, styles)
+ self.list_number += 1
+ self.paragraphs.append(self.current_paragraph)
+ self.run = self.current_paragraph['runs'][-1]
+ self.still_new = True
+
+ def set_attribs(self, classes, styles):
+ if 'dacenter' in classes:
+ self.current_paragraph['params']['align'] = 'center'
+ elif 'daflushright' in classes:
+ self.current_paragraph['params']['align'] = 'end'
+ else:
+ self.current_paragraph['params']['align'] = 'start'
+ if len(classes):
+ if 'daspacingtight' in classes:
+ self.current_paragraph['params']['spacing'] = 240
+ self.current_paragraph['params']['after'] = 0
+ elif 'daspacingsingle' in classes:
+ self.current_paragraph['params']['spacing'] = 240
+ self.current_paragraph['params']['after'] = 240
+ elif 'daspacingdouble' in classes:
+ self.current_paragraph['params']['spacing'] = 480
+ self.current_paragraph['params']['after'] = 0
+ elif 'daspacingoneandahalf' in classes:
+ self.current_paragraph['params']['spacing'] = 260
+ self.current_paragraph['params']['after'] = 0
+ elif 'daspacingtriple' in classes:
+ self.current_paragraph['params']['spacing'] = 700
+ self.current_paragraph['params']['after'] = 0
+ if styles:
+ m = re.search(r'margin-left:([0-9\.]+)px', styles)
+ if m:
+ self.current_paragraph['params']['leftindent'] = 20 * int(m.group(1))
+ m = re.search(r'margin-right:([0-9\.]+)px', styles)
+ if m:
+ self.current_paragraph['params']['rightindent'] = 20 * int(m.group(1))
+ m = re.search(r'text-indent:([0-9\.]+)px', styles)
+ if m:
+ self.current_paragraph['params']['firstline'] = 20 * int(m.group(1))
+
+ def __str__(self):
+ output = ''
+ for para in self.paragraphs:
+ # logmessage("Got a paragraph where style is " + para['params'].get('style', 'undefined') + " and indentation is " + str(para['params'].get('indentation', 'undefined')))
+ output += ' '
+ if 'align' not in para['params']:
+ para['params']['align'] = 'start'
+ if para['params']['align'] == 'center':
+ output += ' '
+ elif para['params']['align'] == 'end':
+ output += ' '
+ if 'spacing' in para['params']:
+ output += ' '
+ if para['params']['style'] == 'ul' or para['params']['style'].startswith('ol'):
+ if 'leftindent' in para['params']:
+ left_indent = para['params']['leftindent']
+ else:
+ left_indent = 36*para['params']['indentation']
+ if 'rightindent' in para['params']:
+ right_indent = para['params']['rightindent']
+ else:
+ right_indent = 0
+ output += ' '
+ elif para['params']['style'] == 'blockquote':
+ if 'spacing' not in para['params']:
+ output += ' '
+ output += ' '
+ elif 'leftindent' in para['params'] or 'rightindent' in para['params'] or 'firstline' in para['params']:
+ if 'leftindent' in para['params']:
+ left_indent = para['params']['leftindent']
+ else:
+ left_indent = 0
+ if 'rightindent' in para['params']:
+ right_indent = para['params']['rightindent']
+ else:
+ right_indent = 0
+ if 'firstline' in para['params']:
+ first_line = para['params']['firstline']
+ else:
+ first_line = 0
+ output += ' '
+ output += ' '
+ if para['params']['style'] == 'ul':
+ output += str(RichText("•\t"))
+ if para['params']['style'] == 'ol1':
+ output += str(RichText(str(para['params']['list_number']) + ".\t"))
+ elif para['params']['style'] == 'olA':
+ output += str(RichText(Alpha(para['params']['list_number']) + ".\t"))
+ elif para['params']['style'] == 'ola':
+ output += str(RichText(alpha(para['params']['list_number']) + ".\t"))
+ elif para['params']['style'] == 'olI':
+ output += str(RichText(Roman_Numeral(para['params']['list_number']) + ".\t"))
+ elif para['params']['style'] == 'oli':
+ output += str(RichText(roman_numeral(para['params']['list_number']) + ".\t"))
+ for run in para['runs']:
+ output += str(run)
+ output += ' '
+ return output
+
+ def start_link(self, url):
+ ref = self.tpl.docx._part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True)
+ self.current_paragraph['runs'].append('' % (ref, ))
+ self.new_run()
+ self.still_new = False
+
+ def end_link(self):
+ self.current_paragraph['runs'].append(' ')
+ self.new_run()
+ self.still_new = False
+
+ def new_run(self):
+ self.current_paragraph['runs'].append(RichText(''))
+ self.run = self.current_paragraph['runs'][-1]
+
+ def traverse(self, elem):
+ for part in elem.contents:
+ if isinstance(part, NavigableString):
+ self.run.add(str(part), italic=self.italic, bold=self.bold, underline=self.underline, strike=self.strike, size=self.size, style=self.charstyle, color=self.color)
+ self.still_new = False
+ elif isinstance(part, Tag):
+ # logmessage("Part name is " + str(part.name))
+ if part.name == 'p':
+ if 'class' in part.attrs:
+ classes = part.attrs['class']
+ else:
+ classes = []
+ if 'style' in part.attrs:
+ styles = part.attrs['style']
+ else:
+ styles = ""
+ self.new_paragraph(classes, styles)
+ if 'dabold' in classes:
+ self.bold = True
+ self.traverse(part)
+ if 'dabold' in classes:
+ self.bold = False
+ elif part.name == 'li':
+ if 'class' in part.attrs:
+ classes = part.attrs['class']
+ else:
+ classes = []
+ if 'style' in part.attrs:
+ styles = part.attrs['style']
+ else:
+ styles = ""
+ self.new_paragraph(classes, styles)
+ self.traverse(part)
+ elif part.name == 'ul':
+ # logmessage("Entering a UL")
+ oldstyle = self.style
+ self.style = 'ul'
+ self.indentation += 10
+ self.traverse(part)
+ self.indentation -= 10
+ self.style = oldstyle
+ # logmessage("Leaving a UL")
+ elif part.name == 'ol':
+ # logmessage("Entering a OL")
+ oldstyle = self.style
+ oldlistnumber = self.list_number
+ oldlisttype = self.list_type
+ if part.get('type', None) in list_types:
+ self.list_type = part['type']
+ else:
+ self.list_type = list_types[(list_types.index(self.list_type) + 1) % 5]
+ try:
+ self.list_number = int(part.get('start', 1))
+ except:
+ self.list_number = 1
+ self.style = 'ol' + self.list_type
+ self.indentation += 10
+ self.traverse(part)
+ self.indentation -= 10
+ self.list_type = oldlisttype
+ self.list_number = oldlistnumber
+ self.style = oldstyle
+ # logmessage("Leaving a OL")
+ elif part.name == 'strong':
+ self.bold = True
+ self.traverse(part)
+ self.bold = False
+ elif part.name == 'em':
+ self.italic = True
+ self.traverse(part)
+ self.italic = False
+ elif part.name == 'strike':
+ self.strike = True
+ self.traverse(part)
+ self.strike = False
+ elif part.name == 'u':
+ self.underline = True
+ self.traverse(part)
+ self.underline = False
+ elif part.name == 'blockquote':
+ oldstyle = self.style
+ self.style = 'blockquote'
+ self.indentation += 20
+ self.traverse(part)
+ self.indentation -= 20
+ self.style = oldstyle
+ elif re.match(r'h[1-6]', part.name):
+ oldsize = self.size
+ self.size = 60 - ((int(part.name[1]) - 1) * 10)
+ if 'class' in part.attrs:
+ classes = part.attrs['class']
+ else:
+ classes = []
+ if 'style' in part.attrs:
+ styles = part.attrs['style']
+ else:
+ styles = ""
+ self.new_paragraph(classes, styles)
+ self.bold = True
+ self.traverse(part)
+ self.bold = False
+ self.size = oldsize
+ elif part.name == 'a':
+ self.start_link(part['href'])
+ if self.tpl.da_hyperlink_style:
+ self.charstyle = self.tpl.da_hyperlink_style
+ else:
+ self.underline = True
+ self.color = '#0000ff'
+ self.traverse(part)
+ if self.tpl.da_hyperlink_style:
+ self.charstyle = None
+ else:
+ self.underline = False
+ self.color = None
+ self.end_link()
+ elif part.name == 'br':
+ self.run.add("\n", italic=self.italic, bold=self.bold, underline=self.underline, strike=self.strike, size=self.size, style=self.charstyle, color=self.color)
+ self.still_new = False
+ else:
+ logmessage("Encountered a " + part.__class__.__name__)
+
+
+class InlineSoupParser:
+
+ def __init__(self, tpl):
+ self.runs = [RichText('')]
+ self.run = self.runs[-1]
+ self.bold = False
+ self.italic = False
+ self.underline = False
+ self.indentation = 0
+ self.style = 'p'
+ self.strike = False
+ self.size = None
+ self.charstyle = None
+ self.color = None
+ self.tpl = tpl
+ self.at_start = True
+ self.list_number = 1
+ self.list_type = list_types[-1]
+
+ def new_paragraph(self):
+ if self.at_start:
+ self.at_start = False
+ else:
+ self.run.add("\n", italic=self.italic, bold=self.bold, underline=self.underline, strike=self.strike, size=self.size, style=self.charstyle, color=self.color)
+ if self.indentation:
+ self.run.add("\t" * self.indentation)
+ if self.style == 'ul':
+ self.run.add("•\t")
+ if self.style == 'ol1':
+ self.run.add(str(self.list_number) + ".\t")
+ self.list_number += 1
+ elif self.style == 'olA':
+ self.run.add(Alpha(self.list_number) + ".\t")
+ self.list_number += 1
+ elif self.style == 'ola':
+ self.run.add(alpha(self.list_number) + ".\t")
+ self.list_number += 1
+ elif self.style == 'olI':
+ self.run.add(Roman_Numeral(self.list_number) + ".\t")
+ self.list_number += 1
+ elif self.style == 'oli':
+ self.run.add(roman_numeral(self.list_number) + ".\t")
+ self.list_number += 1
+ # else:
+ # self.list_number = 1
+
+ def __str__(self):
+ output = ''
+ for run in self.runs:
+ output += str(run)
+ return output
+
+ def start_link(self, url):
+ ref = self.tpl.docx._part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True)
+ self.runs.append('' % (ref, ))
+ self.new_run()
+
+ def end_link(self):
+ self.runs.append(' ')
+ self.new_run()
+
+ def new_run(self):
+ self.runs.append(RichText(''))
+ self.run = self.runs[-1]
+
+ def traverse(self, elem):
+ for part in elem.contents:
+ if isinstance(part, NavigableString):
+ self.run.add(str(part), italic=self.italic, bold=self.bold, underline=self.underline, strike=self.strike, size=self.size, style=self.charstyle, color=self.color)
+ elif isinstance(part, Tag):
+ if part.name in ('p', 'blockquote'):
+ self.new_paragraph()
+ self.traverse(part)
+ elif part.name == 'li':
+ self.new_paragraph()
+ self.traverse(part)
+ elif part.name == 'ul':
+ oldstyle = self.style
+ self.style = 'ul'
+ self.indentation += 1
+ self.traverse(part)
+ self.indentation -= 1
+ self.style = oldstyle
+ elif part.name == 'ol':
+ oldstyle = self.style
+ oldlistnumber = self.list_number
+ oldlisttype = self.list_type
+ if part.get('type', None) in list_types:
+ self.list_type = part['type']
+ else:
+ self.list_type = list_types[(list_types.index(self.list_type) + 1) % 5]
+ try:
+ self.list_number = int(part.get('start', 1))
+ except:
+ self.list_number = 1
+ self.style = 'ol' + self.list_type
+ self.indentation += 1
+ self.traverse(part)
+ self.indentation -= 1
+ self.list_type = oldlisttype
+ self.list_number = oldlistnumber
+ self.style = oldstyle
+ elif part.name == 'strong':
+ self.bold = True
+ self.traverse(part)
+ self.bold = False
+ elif part.name == 'em':
+ self.italic = True
+ self.traverse(part)
+ self.italic = False
+ elif part.name == 'strike':
+ self.strike = True
+ self.traverse(part)
+ self.strike = False
+ elif part.name == 'u':
+ self.underline = True
+ self.traverse(part)
+ self.underline = False
+ elif re.match(r'h[1-6]', part.name):
+ oldsize = self.size
+ self.size = 60 - ((int(part.name[1]) - 1) * 10)
+ self.bold = True
+ self.traverse(part)
+ self.bold = False
+ self.size = oldsize
+ elif part.name == 'a':
+ self.start_link(part['href'])
+ if self.tpl.da_hyperlink_style:
+ self.charstyle = self.tpl.da_hyperlink_style
+ else:
+ self.underline = True
+ self.color = '#0000ff'
+ self.traverse(part)
+ if self.tpl.da_hyperlink_style:
+ self.charstyle = None
+ else:
+ self.underline = False
+ self.color = None
+ self.end_link()
+ elif part.name == 'br':
+ self.run.add("\n", italic=self.italic, bold=self.bold, underline=self.underline, strike=self.strike, size=self.size, style=self.charstyle, color=self.color)
+ else:
+ logmessage("Encountered a " + part.__class__.__name__)
+
+
+def inline_markdown_to_docx(text, question, tpl):
+ old_context = this_thread.evaluation_context
+ this_thread.evaluation_context = None
+ try:
+ text = str(text)
+ except:
+ this_thread.evaluation_context = old_context
+ raise
+ this_thread.evaluation_context = old_context
+ source_code = markdown_to_html(text, do_terms=False)
+ source_code = re.sub(r"\n", ' ', source_code)
+ source_code = re.sub(r">\s+<", '><', source_code)
+ soup = BeautifulSoup('' + source_code + '', 'html.parser')
+ parser = InlineSoupParser(tpl)
+ for elem in soup.find_all(recursive=False):
+ parser.traverse(elem)
+ output = str(parser)
+ return docx_template_filter(output, question=question, replace_newlines=False)
+
+
+def markdown_to_docx(text, question, tpl):
+ old_context = this_thread.evaluation_context
+ this_thread.evaluation_context = None
+ try:
+ text = str(text)
+ except:
+ this_thread.evaluation_context = old_context
+ raise
+ this_thread.evaluation_context = old_context
+ if daconfig.get('new markdown to docx', False):
+ source_code = markdown_to_html(text, do_terms=False)
+ source_code = re.sub(r"\n", ' ', source_code)
+ source_code = re.sub(r">\s+<", '><', source_code)
+ soup = BeautifulSoup('' + source_code + '', 'html.parser')
+ parser = SoupParser(tpl)
+ for elem in soup.find_all(recursive=False):
+ parser.traverse(elem)
+ output = str(parser)
+ # logmessage(output)
+ return docx_template_filter(output, question=question)
+ return inline_markdown_to_docx(text, question, tpl)
+
+
+def include_docx_template(template_file, **kwargs):
+ """Include the contents of one docx file inside another docx file."""
+ use_jinja = kwargs.pop('_use_jinja2', True)
+ if this_thread.evaluation_context is None:
+ return 'ERROR: not in a docx file'
+ if template_file.__class__.__name__ in ('DAFile', 'DAFileList', 'DAFileCollection', 'DALocalFile', 'DAStaticFile'):
+ template_path = template_file.path()
+ else:
+ template_path = package_template_filename(template_file, package=this_thread.current_package)
+ sd = this_thread.misc['docx_template'].new_subdoc()
+ sd.subdocx = docx.Document(template_path)
+ change_numbering = bool(kwargs.pop('change_numbering', True))
+ if '_inline' in kwargs:
+ single_paragraph = True
+ del kwargs['_inline']
+ else:
+ single_paragraph = False
+
+ # We need to keep a copy of the subdocs so we can fix up the master template in the end (in parse.py)
+ # Given we're half way through processing the template, we can't fix the master template here
+ # we have to do it in post
+ if 'docx_subdocs' not in this_thread.misc:
+ this_thread.misc['docx_subdocs'] = []
+ this_thread.misc['docx_subdocs'].append({'subdoc': deepcopy(sd.subdocx), 'change_numbering': change_numbering})
+
+ # Fix the subdocs before they are included in the template
+ fix_subdoc(this_thread.misc['docx_template'], {'subdoc': sd.subdocx, 'change_numbering': change_numbering})
+
+ first_paragraph = sd.subdocx.paragraphs[0]
+
+ if not use_jinja:
+ if single_paragraph:
+ return re.sub(r']*>\s*(.*) \s*', r'\1', sanitize_xml(str(first_paragraph._p.xml)), flags=re.DOTALL)
+ return sanitize_xml(str(sd))
+
+ for key, val in kwargs.items():
+ if hasattr(val, 'instanceName'):
+ the_repr = val.instanceName
+ elif isinstance(val, (int, float, bool, NoneType)):
+ the_repr = val
+ else:
+ the_repr = '_codecs.decode(_array.array("b", "' + re.sub(r'\n', '', codecs.encode(bytearray(val, encoding='utf-8'), 'base64').decode()) + '".encode()), "base64").decode()'
+ first_paragraph.insert_paragraph_before(str("{%%p set %s = %s %%}" % (key, the_repr)))
+ if 'docx_include_count' not in this_thread.misc:
+ this_thread.misc['docx_include_count'] = 0
+ this_thread.misc['docx_include_count'] += 1
+ if single_paragraph:
+ return re.sub(r']*>\s*(.*) \s*', r'\1', str(first_paragraph._p.xml), flags=re.DOTALL)
+ return sd
diff --git a/docassemble_base/docassemble/base/filter/docx_subdoc.py b/docassemble_base/docassemble/base/filter/docx_subdoc.py
new file mode 100644
index 000000000..bb75244fd
--- /dev/null
+++ b/docassemble_base/docassemble/base/filter/docx_subdoc.py
@@ -0,0 +1,39 @@
+from docx.oxml.section import CT_SectPr
+from docx.oxml.table import CT_Tbl
+from docxcompose.composer import Composer
+
+def fix_subdoc(masterdoc, subdoc_info):
+ """Fix the images, styles, references, shapes, etc of a subdoc"""
+ for section in masterdoc.sections:
+ for part in section.part.package.parts:
+ if part.content_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml" and not isinstance(part._blob, bytes):
+ part._blob = part._blob.encode('utf-8')
+ subdoc = subdoc_info['subdoc']
+ change_numbering = subdoc_info['change_numbering']
+ composer = Composer(masterdoc) # Using docxcompose
+ composer.reset_reference_mapping()
+
+ # This is the same as the docxcompose function, except it doesn't copy the elements over.
+ # Copying the elements over is done by returning the subdoc XML in this function.
+ # Both sd.subdocx and the master template file are changed with these functions.
+ composer._create_style_id_mapping(subdoc)
+ for element in subdoc.element.body:
+ if isinstance(element, CT_SectPr):
+ continue
+ composer.add_referenced_parts(subdoc.part, masterdoc.part, element)
+ composer.add_styles(subdoc, element)
+ if change_numbering and not isinstance(element, CT_Tbl):
+ try:
+ composer.add_numberings(subdoc, element)
+ composer.restart_first_numbering(subdoc, element)
+ except:
+ pass
+ composer.add_images(subdoc, element)
+ composer.add_shapes(subdoc, element)
+ composer.add_footnotes(subdoc, element)
+ composer.remove_header_and_footer_references(subdoc, element)
+
+ composer.add_styles_from_other_parts(subdoc)
+ composer.renumber_bookmarks()
+ composer.renumber_docpr_ids()
+ composer.fix_section_types(subdoc)
diff --git a/docassemble_base/docassemble/base/filter/html.py b/docassemble_base/docassemble/base/filter/html.py
new file mode 100644
index 000000000..b3c22a3d1
--- /dev/null
+++ b/docassemble_base/docassemble/base/filter/html.py
@@ -0,0 +1,959 @@
+import re
+import os
+import mimetypes
+import codecs
+import json
+from io import BytesIO
+import xml.etree.ElementTree as ET
+import qrcode
+import qrcode.image.svg
+from pikepdf import Pdf
+import PIL
+from bs4 import BeautifulSoup
+from ..functions import get_config
+from ..hooks import (
+ file_finder,
+ get_default_thead_class,
+ fg_make_png_for_pdf_path,
+ url_finder,
+ get_saved_file_class,
+ get_default_table_class,
+ fg_make_pdf_for_word_path,
+)
+from ..language.control import get_language
+from ..language.words import word
+from ..thread_context import this_thread
+from .utils import convert_length, replace_fields, repeat_along
+
+QPDF_PATH = 'qpdf'
+NoneType = type(None)
+
+term_start = re.compile(r'\[\[')
+term_match = re.compile(r'\[\[([^\[\]\|]*)(\|[^\[\]]*)?\]\]', re.DOTALL)
+noquote_match = re.compile(r'"')
+lt_match = re.compile(r'<')
+gt_match = re.compile(r'>')
+amp_match = re.compile(r'&')
+# amp_match = re.compile(r'&(?!#?[0-9A-Za-z]+;)')
+emoji_match = re.compile(r':([A-Za-z][A-Za-z0-9\_\-]+):')
+extension_match = re.compile(r'\.[a-z]+$')
+map_match = re.compile(r'\[MAP ([^\]]+)\]', flags=re.DOTALL)
+code_match = re.compile(r'')
+
+# def blank_da_send_mail(*args, **kwargs):
+# logmessage("da_send_mail: no mail agent configured!")
+# return(None)
+
+# da_send_mail = blank_da_send_mail
+
+# def set_da_send_mail(func):
+# global da_send_mail
+# da_send_mail = func
+# return
+
+# def blank_file_finder(*args, **kwargs):
+# return({'filename': "invalid"})
+
+# file_finder = blank_file_finder
+
+# def set_file_finder(func):
+# global file_finder
+# #logmessage("set the file finder to " + str(func))
+# file_finder = func
+# return
+
+# def blank_url_finder(*args, **kwargs):
+# return('about:blank')
+
+# url_finder = blank_url_finder
+
+# def set_url_finder(func):
+# global url_finder
+# url_finder = func
+# return
+
+# def blank_url_for(*args, **kwargs):
+# return('about:blank')
+
+# url_for = blank_url_for
+
+# def set_url_for(func):
+# global url_for
+# url_for = func
+# return
+
+
+
+def html_filter(text, status=None, question=None, embedder=None, default_image_width=None, external=False):
+ if question is None and status is not None:
+ question = status.question
+ text = text + "\n\n"
+ text = re.sub(r'^[|] (.*)$', r'\1
', text, flags=re.MULTILINE)
+ text = replace_fields(text, status=status, embedder=embedder)
+ # if embedder is not None:
+ # text = re.sub(r'\[FIELD ([^\]]+)\]', lambda x: embedder(status, x.group(1)), text)
+ # else:
+ # text = re.sub(r'\[FIELD ([^\]]+)\]', 'ERROR: FIELD cannot be used here', text)
+ text = re.sub(r'\[TARGET ([^\]]+)\]', target_html, text)
+ if this_thread.evaluation_context != 'docx':
+ text = re.sub(r'\[EMOJI ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_url_string(x, emoji=True, question=question, external=external, status=status), text)
+ text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', lambda x: image_url_string(x, question=question, external=external, status=status), text)
+ text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_url_string(x, question=question, external=external, status=status), text)
+ text = re.sub(r'\[FILE ([^,\]]+)\]', lambda x: image_url_string(x, question=question, default_image_width=default_image_width, external=external, status=status), text)
+ text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', qr_url_string, text)
+ text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+)\]', qr_url_string, text)
+ text = re.sub(r'\[QR ([^,\]]+)\]', qr_url_string, text)
+ if map_match.search(text):
+ text = map_match.sub((lambda x: map_string(x.group(1), status)), text)
+ # width="420" height="315"
+ text = re.sub(r'\[YOUTUBE ([^\]]+)\]', r'', text)
+ text = re.sub(r'\[YOUTUBE4:3 ([^\]]+)\]', r'', text)
+ text = re.sub(r'\[YOUTUBE16:9 ([^\]]+)\]', r'', text)
+ # width="500" height="281"
+ text = re.sub(r'\[VIMEO ([^\]]+)\]', r'', text)
+ text = re.sub(r'\[VIMEO4:3 ([^\]]+)\]', r'', text)
+ text = re.sub(r'\[VIMEO16:9 ([^\]]+)\]', r'', text)
+ text = re.sub(r'\[BEGIN_CAPTION\](.+?)\[VERTICAL_LINE\]\s*(.+?)\[END_CAPTION\]', html_caption, text, flags=re.DOTALL)
+ text = re.sub(r'\[BEGIN_TWOCOL\](.+?)\[BREAK\]\s*(.+?)\[END_TWOCOL\]', html_two_col, text, flags=re.DOTALL)
+ text = re.sub(r'\[NBSP\]', r' ', text)
+ text = re.sub(r'\[REDACTION_SPACE\]', '█', text)
+ text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', lambda x: repeat_along('█', x), text)
+ text = re.sub(r'\[ENDASH\]', r'–', text)
+ text = re.sub(r'\[EMDASH\]', r'—', text)
+ text = re.sub(r'\[HYPHEN\]', r'-', text)
+ text = re.sub(r'\[CHECKBOX\]', r' ', text)
+ text = re.sub(r'\[BLANK\]', r' ', text)
+ text = re.sub(r'\[BLANKFILL\]', r' ', text)
+ text = re.sub(r'\[PAGEBREAK\] *', r'', text)
+ text = re.sub(r'\[PAGENUM\] *', r'', text)
+ text = re.sub(r'\[SECTIONNUM\] *', r'', text)
+ text = re.sub(r'\[SKIPLINE\] *', r'
', text)
+ text = re.sub(r'\[NEWLINE\] *', r'
', text)
+ text = re.sub(r'\[NEWPAR\] *', r'
', text)
+ text = re.sub(r'\[BR\] *', r'
', text)
+ text = re.sub(r'\[TAB\] *', '', text)
+ text = re.sub(r' *\[END\] *', r'\n', text)
+ lines = re.split(r'\n *\n', text)
+ text = ''
+ spacing_class = None
+ doing_indentation = False
+ for line in lines:
+ classes = set()
+ styles = {}
+ if re.search(r'\[TIGHTSPACING\]', line):
+ spacing_class = 'daspacingtight'
+ if re.search(r'\[SINGLESPACING\]', line):
+ spacing_class = 'daspacingsingle'
+ if re.search(r'\[DOUBLESPACING\]', line):
+ spacing_class = 'daspacingdouble'
+ if re.search(r'\[ONEANDAHALFSPACING\]', line):
+ spacing_class = 'daspacingoneandahalf'
+ if re.search(r'\[TRIPLESPACING\]', line):
+ spacing_class = 'daspacingtriple'
+ if re.search(r'\[START_INDENTATION\]', line):
+ doing_indentation = True
+ if re.search(r'\[STOP_INDENTATION\]', line):
+ doing_indentation = False
+ if spacing_class:
+ classes.add(spacing_class)
+ if doing_indentation and not re.search(r'\[NOINDENT\]', line):
+ styles['text-indent'] = '36px'
+ if re.search(r'\[BORDER\]', line):
+ classes.add('daborder')
+ if re.search(r'\[FLUSHLEFT\]', line):
+ classes.add('daflushleft')
+ if re.search(r'\[FLUSHRIGHT\]', line):
+ classes.add('daflushright')
+ if re.search(r'\[CENTER\]', line):
+ classes.add('dacenter')
+ if re.search(r'\[BOLDCENTER\]', line):
+ classes.add('dacenter')
+ classes.add('dabold')
+ m = re.search(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+)\]', line)
+ if m:
+ styles["padding-left"] = str(convert_length(m.group(1), 'px')) + 'px'
+ m = re.search(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+) *([0-9]+ *[A-Za-z]+)\]', line)
+ if m:
+ styles["margin-left"] = str(convert_length(m.group(1), 'px')) + 'px'
+ styles["margin-right"] = str(convert_length(m.group(2), 'px')) + 'px'
+ orig_length = len(line)
+ line = re.sub(r'\[(BORDER|NOINDENT|FLUSHLEFT|FLUSHRIGHT|BOLDCENTER|CENTER|TIGHTSPACING|SINGLESPACING|DOUBLESPACING|ONEANDAHALFSPACING|TRIPLESPACING|START_INDENTATION|STOP_INDENTATION)\] *', r'', line)
+ line = re.sub(r'\[INDENTBY[^\]]*\] *', r'', line)
+ if orig_length > 0 and len(line) == 0:
+ continue
+ if line.startswith('>'):
+ line = re.sub(r'^> *', '', line)
+ text += "> "
+ if len(classes) > 0 or len(styles) > 0:
+ text += ' 0:
+ text += ' style="' + "".join(map(lambda x: str(x[0]) + ":" + x[1] + ';', styles.items())) + '"'
+ text += '>'
+ text += line + '\n\n'
+ text = re.sub(r'\n+$', r'', text)
+ return text
+
+
+def map_string(encoded_text, status):
+ if status is None:
+ return ''
+ map_number = len(status.maps)
+ status.maps.append(codecs.decode(bytearray(encoded_text, 'utf-8'), 'base64').decode())
+ return ''
+
+
+def target_html(match):
+ target = match.group(1)
+ target = re.sub(r'[^A-Za-z0-9\_]', r'', str(target))
+ return ''
+
+
+def html_caption(match):
+ firstcol = match.group(1)
+ secondcol = match.group(2)
+ firstcol = re.sub(r'^\s+', '', firstcol)
+ firstcol = re.sub(r'\s+$', '', firstcol)
+ secondcol = re.sub(r'^\s+', '', secondcol)
+ secondcol = re.sub(r'\s+$', '', secondcol)
+ firstcol = markdown_to_html(firstcol)
+ secondcol = markdown_to_html(secondcol)
+ return '' + firstcol + ' ' + secondcol + '
'
+
+
+def html_two_col(match):
+ firstcol = markdown_to_html(match.group(1))
+ secondcol = markdown_to_html(match.group(2))
+ return '' + firstcol + ' ' + secondcol + '
'
+
+
+def add_newlines(string):
+ string = re.sub(r'\[(BR)\]', r'[NEWLINE]', string)
+ string = re.sub(r' *\n', r'\n', string)
+ string = re.sub(r'(? 0:
+ return audio_control(urls)
+ return ''
+ if re.search(r'^video', file_info['mimetype']):
+ urls = get_video_urls([{'text': "[FILE " + file_reference + "]", 'package': None, 'type': 'video'}], question=question)
+ if len(urls) > 0:
+ return video_control(urls)
+ return ''
+ if 'extension' in file_info and file_info['extension'] is not None:
+ if re.match(r'.*%$', width):
+ width_string = "width:" + width
+ stack_width_string = width_string
+ else:
+ width_string = "max-width:" + width
+ stack_width_string = "width:" + width
+ if emoji:
+ width_string += ';vertical-align: middle'
+ alt_text = 'alt="" '
+ the_url = url_finder(file_reference, _question=question, display_filename=file_info['filename'], _external=external)
+ if the_url is None:
+ return '[ERROR: File reference ' + str(file_reference) + ' cannot be displayed]'
+ if width_string == 'width:100%':
+ extra_class = ' dawideimage'
+ else:
+ extra_class = ''
+ if file_info.get('extension', '') in ('png', 'jpg', 'gif', 'svg', 'jpe', 'jpeg'):
+ try:
+ if file_info.get('extension', '') == 'svg':
+ attributes = ET.parse(file_info['fullpath']).getroot().attrib
+ layout_width = attributes['width']
+ layout_height = attributes['height']
+ else:
+ with PIL.Image.open(file_info['fullpath']) as im:
+ layout_width, layout_height = im.size
+ return '
'
+ except:
+ return '
'
+ if file_info['extension'] in ('pdf', 'docx', 'rtf', 'doc', 'odt'):
+ if file_info['extension'] in ('docx', 'rtf', 'doc', 'odt') and not os.path.isfile(file_info['path'] + '.pdf'):
+ fg_make_pdf_for_word_path(file_info['path'], file_info['extension'])
+ fg_make_png_for_pdf_path(file_info['path'] + ".pdf", 'screen', page=1)
+ if re.match(r'[0-9]+', str(file_reference)):
+ sf = get_saved_file_class()(int(file_reference), fix=True)
+ sf.finalize()
+ if 'pages' not in file_info:
+ try:
+ with Pdf.open(file_info['path'] + '.pdf') as reader:
+ file_info['pages'] = len(reader.pages)
+ except:
+ file_info['pages'] = 1
+ the_image_url = url_finder(file_reference, size="screen", page=1, _question=question, _external=external)
+ if the_image_url is None:
+ return '[ERROR: File reference ' + str(file_reference) + ' cannot be displayed]'
+ if 'filename' in file_info:
+ title = ' title="' + file_info['filename']
+ if 'pages' in file_info and file_info['pages'] > 1:
+ title += " (" + str(file_info['pages']) + " " + word('pages') + ")"
+ title += '"'
+ else:
+ if 'pages' in file_info and file_info['pages'] > 1:
+ title = ' title="' + str(file_info['pages']) + " " + word('pages') + '"'
+ else:
+ title = ''
+ if alt_text == '':
+ the_alt_text = 'alt=' + json.dumps(word("Thumbnail image of document")) + ' '
+ else:
+ the_alt_text = alt_text
+ try:
+ with Pdf.open(file_info['path'] + '.pdf') as reader:
+ layout_width = reader.pages[0].mediabox[2] - reader.pages[0].mediabox[0]
+ layout_height = reader.pages[0].mediabox[3] - reader.pages[0].mediabox[1]
+ if width_string == 'width:100%':
+ output = '
'
+ else:
+ if 'pages' in file_info and file_info['pages'] >= 1:
+ extra_pages = min(2, file_info['pages'] - 1)
+ else:
+ extra_pages = 2
+ aspect_ratio = 1.0*layout_width/layout_height
+ stack_width_string += "; height: auto; aspect-ratio: " + str(aspect_ratio) + ";"
+ output = ''
+ except:
+ output = '
'
+ return output
+ return '' + file_info['filename'] + ''
+ return '[Invalid image reference; reference=' + str(file_reference) + ', width=' + str(width) + ', filename=' + file_info.get('filename', 'unknown') + ']'
+
+
+def qr_url_string(match):
+ string = match.group(1)
+ try:
+ width = match.group(2)
+ assert width != 'None'
+ except:
+ width = "300px"
+ if width == "full":
+ width = "300px"
+ if match.lastindex == 3:
+ if match.group(3) != 'None':
+ alt_text = str(match.group(3))
+ else:
+ alt_text = word(f"A QR code that goes to {string}")
+ else:
+ alt_text = word(f"A QR code that goes to {string}")
+ width_string = "width:" + width
+ im = qrcode.make(string, image_factory=qrcode.image.svg.SvgPathFillImage)
+ output = BytesIO()
+ im.save(output)
+ the_image = output.getvalue().decode()
+ the_image = re.sub(r"<\?xml version='1.0' encoding='UTF-8'\?>\n", '', the_image)
+ the_image = re.sub(r'height="[0-9]+mm" ', '', the_image)
+ the_image = re.sub(r'width="[0-9]+mm" ', '', the_image)
+ m = re.search(r'(viewBox="[^"]+")', the_image)
+ if m:
+ viewbox = m.group(1)
+ else:
+ viewbox = ""
+ return ''
+
+
+def get_icon_html(text):
+ icons_setting = get_config('default icons', None)
+ if icons_setting == 'font awesome':
+ m = re.search(r'^(fa[a-z])-fa-(.*)', text)
+ if m:
+ the_prefix = m.group(1)
+ text = m.group(2)
+ else:
+ the_prefix = get_config('font awesome prefix', 'fa-solid')
+ if the_prefix == 'fab':
+ the_prefix = 'fa-brands'
+ elif the_prefix == 'far':
+ the_prefix = 'fa-regular'
+ elif the_prefix == 'fas':
+ the_prefix = 'fa-solid'
+ return ''
+ if icons_setting == 'material icons':
+ return '' + str(text) + ''
+ return None
+
+
+def emoji_html(text, status=None, question=None, images=None):
+ # logmessage("Got to emoji_html")
+ if status is not None and question is None:
+ question = status.question
+ if images is None:
+ images = question.interview.images
+ if text in images:
+ if status is not None and images[text].attribution is not None:
+ status.attributions.add(images[text].attribution)
+ return image_url(images[text].get_reference(), word('icon'), '1em', emoji=True, question=question)
+ icon_html = get_icon_html(text)
+ if icon_html:
+ return icon_html
+ return ":" + str(text) + ":"
+
+
+def emoji_insert(text, status=None, images=None):
+ if images is None:
+ images = status.question.interview.images
+ if text in images:
+ if status is not None and images[text].attribution is not None:
+ status.attributions.add(images[text].attribution)
+ return "[EMOJI " + images[text].get_reference() + ', 1.2em]'
+ return ":" + str(text) + ":"
+
+
+def link_rewriter(m, status):
+ the_path = None
+ if m.group(1).startswith('#'):
+ return ' 0:
+ lang = get_language()
+ for term in question.terms:
+ terms_done.add(term.lower())
+ # logmessage("Searching for term " + term + " in " + a)
+ if lang in question.terms[term]['re']:
+ a = question.terms[term]['re'][lang].sub(sub_term, a)
+ else:
+ a = question.terms[term]['re'][question.language].sub(sub_term, a)
+ # logmessage("string is now " + str(a))
+ if len(question.autoterms) > 0:
+ lang = get_language()
+ for term in question.autoterms:
+ if term.lower() in terms_done:
+ continue
+ terms_done.add(term.lower())
+ # logmessage("Searching for term " + term + " in " + a)
+ if lang in question.autoterms[term]['re']:
+ a = question.autoterms[term]['re'][lang].sub(r'[[\1]]', a)
+ else:
+ a = question.autoterms[term]['re'][question.language].sub(r'[[\1]]', a)
+ # logmessage("string is now " + str(a))
+ if 'interview_terms' in status.extras:
+ interview_terms = status.extras['interview_terms']
+ else:
+ interview_terms = question.interview.terms
+ if 'interview_autoterms' in status.extras:
+ interview_autoterms = status.extras['interview_autoterms']
+ else:
+ interview_autoterms = question.interview.autoterms
+ else:
+ interview_terms = question.interview.terms
+ interview_autoterms = question.interview.autoterms
+ if len(interview_terms) > 0:
+ lang = get_language()
+ if lang in interview_terms and len(interview_terms[lang]) > 0:
+ for term in interview_terms[lang]:
+ if term.lower() in terms_done:
+ continue
+ terms_done.add(term.lower())
+ # logmessage("Searching for term " + term + " in " + a)
+ a = interview_terms[lang][term]['re'].sub(sub_term, a)
+ # logmessage("string is now " + str(a))
+ elif question.language in interview_terms and len(interview_terms[question.language]) > 0:
+ for term in interview_terms[question.language]:
+ if term.lower() in terms_done:
+ continue
+ terms_done.add(term.lower())
+ # logmessage("Searching for term " + term + " in " + a)
+ a = interview_terms[question.language][term]['re'].sub(sub_term, a)
+ # logmessage("string is now " + str(a))
+ if len(interview_autoterms) > 0:
+ lang = get_language()
+ if lang in interview_autoterms and len(interview_autoterms[lang]) > 0:
+ for term in interview_autoterms[lang]:
+ if term.lower() in terms_done:
+ continue
+ terms_done.add(term.lower())
+ # logmessage("Searching for term " + term + " in " + a)
+ a = interview_autoterms[lang][term]['re'].sub(r'[[\1]]', a)
+ # logmessage("string is now " + str(a))
+ elif question.language in interview_autoterms and len(interview_autoterms[question.language]) > 0:
+ for term in interview_autoterms[question.language]:
+ if term.lower() in terms_done:
+ continue
+ terms_done.add(term.lower())
+ # logmessage("Searching for term " + term + " in " + a)
+ a = interview_autoterms[question.language][term]['re'].sub(r'[[\1]]', a)
+ # logmessage("string is now " + str(a))
+ a = html_filter(str(a), status=status, question=question, embedder=embedder, default_image_width=default_image_width, external=external)
+ # logmessage("before: " + a)
+ if status and status.extras.get('tableCssClass', None):
+ classes = status.extras['tableCssClass'].split(',')
+ table_class = json.dumps(classes[0].strip())
+ if len(classes) > 1:
+ thead_class = json.dumps(classes[1].strip())
+ else:
+ thead_class = None
+ else:
+ table_class = get_default_table_class()
+ thead_class = get_default_thead_class()
+ a = re.sub(r'<(/?)table', r'<\1TABLE', a)
+ a = re.sub(r'', r'', a)
+ if use_pandoc:
+ from docassemble.base import pandoc
+ converter = pandoc.MyPandoc()
+ converter.output_format = 'html'
+ converter.input_content = a
+ converter.convert(question)
+ result = converter.output_content
+ else:
+ try:
+ result = this_thread.markdown.reset().convert(a)
+ except:
+ # Try again because sometimes it fails randomly and maybe trying again will work.
+ result = this_thread.markdown.reset().convert(a)
+ result = re.sub(r'', r'', result)
+ if thead_class != '':
+ result = re.sub(r'', r'', result)
+ result = re.sub(r'
', r'
', result)
+ result = re.sub(r'<(/?)TABLE', r'<\1table', result)
+ result = re.sub(r'', r'', result)
+ result = re.sub(r'<(t[dh]) align="(right|left|center)">', r'<\1 class="text-\2">', result)
+ result = re.sub(r'', r'', result)
+ result = re.sub(r' 0 and 'terms' in status.extras:
+ result = term_match.sub((lambda x: add_terms(x.group(1), status.extras['terms'], label=x.group(2), status=status, question=question)), result)
+ if len(question.autoterms) > 0 and 'autoterms' in status.extras:
+ result = term_match.sub((lambda x: add_terms(x.group(1), status.extras['autoterms'], label=x.group(2), status=status, question=question)), result)
+ if 'interview_terms' in status.extras:
+ interview_terms = status.extras['interview_terms']
+ else:
+ interview_terms = question.interview.terms
+ if 'interview_autoterms' in status.extras:
+ interview_autoterms = status.extras['interview_autoterms']
+ else:
+ interview_autoterms = question.interview.autoterms
+ else:
+ interview_terms = question.interview.terms
+ interview_autoterms = question.interview.autoterms
+ if lang in interview_terms and len(interview_terms[lang]):
+ result = term_match.sub((lambda x: add_terms(x.group(1), interview_terms[lang], label=x.group(2), status=status, question=question)), result)
+ elif question.language in interview_terms and len(interview_terms[question.language]):
+ result = term_match.sub((lambda x: add_terms(x.group(1), interview_terms[question.language], label=x.group(2), status=status, question=question)), result)
+ if lang in interview_autoterms and len(interview_autoterms[lang]):
+ result = term_match.sub((lambda x: add_terms(x.group(1), interview_autoterms[lang], label=x.group(2), status=status, question=question)), result)
+ elif question.language in interview_autoterms and len(interview_autoterms[question.language]):
+ result = term_match.sub((lambda x: add_terms(x.group(1), interview_autoterms[question.language], label=x.group(2), status=status, question=question)), result)
+ do_not_scan_for_emojis = bool(re.search(r'\[NO_EMOJIS\]', result))
+ if do_not_scan_for_emojis:
+ result = re.sub(r'\[NO_EMOJIS\]\s*', r'', result)
+ if status is not None and question.interview.scan_for_emojis and not do_not_scan_for_emojis:
+ result = emoji_match.sub((lambda x: emoji_html(x.group(1), status=status, question=question)), result)
+ result = re.sub(r'', result)
+ if trim:
+ if result.startswith('
') and result.endswith('
'):
+ result = re.sub(r'\s*', ' ', result[3:-4])
+ elif pclass:
+ result = re.sub('
', '
', result)
+ if escape:
+ if escape is True:
+ result = noquote_match.sub('"', result)
+ if escape == 'option':
+ result = re.sub(r'\n\r', ' ', BeautifulSoup(result, 'html.parser').get_text()).strip()
+ result = lt_match.sub('<', result)
+ result = gt_match.sub('>', result)
+ if escape is True:
+ result = amp_match.sub('&', result)
+ # logmessage("after: " + result)
+ # result = result.replace('\n', ' ')
+ if result:
+ if strip_newlines:
+ result = result.replace('\n', ' ')
+ if divclass is not None:
+ result = '
' + result + ''
+ # if indent and not code_match.search(result):
+ # return (" " * indent) + re.sub(r'\n', "\n" + (" " * indent), result).rstrip() + "\n"
+ return result
+
+
+def my_escape(result):
+ result = noquote_match.sub('"', result)
+ result = lt_match.sub('<', result)
+ result = gt_match.sub('>', result)
+ result = amp_match.sub('&', result)
+ return result
+
+
+def noquote(string):
+ # return json.dumps(string.replace('\n', ' ').rstrip())
+ return '"' + string.replace('\n', ' ').replace('"', '"').rstrip() + '"'
+
+
+def add_terms_mako(termname, terms, status=None, question=None):
+ lower_termname = re.sub(r'\s+', ' ', str(termname).lower(), re.DOTALL)
+ if lower_termname in terms:
+ term_as_text = to_text(markdown_to_html(str(termname), trim=False, do_terms=False, status=status, question=question), None, None)
+ return '' + str(termname) + ''
+ # logmessage(lower_termname + " is not in terms dictionary")
+ return '[[' + termname + ']]'
+
+
+def add_terms(termname, terms, label=None, status=None, question=None):
+ if label is None:
+ label = str(termname)
+ else:
+ label = re.sub(r'^\|', '', label)
+ lower_termname = re.sub(r'\s+', ' ', termname.lower(), re.DOTALL)
+ if lower_termname in terms:
+ term_as_text = to_text(markdown_to_html(label, trim=False, do_terms=False, status=status, question=question), None, None)
+ return '' + label + ''
+ return '[[' + termname + ']]'
+
+
+def audio_control(files, preload="metadata", title_text=None):
+ for d in files:
+ if isinstance(d, str):
+ return d
+ if title_text is None:
+ title_text = ''
+ else:
+ title_text = " title=" + json.dumps(title_text)
+ output = '\n"
+ return output
+
+
+def video_control(files):
+ for d in files:
+ if isinstance(d, (str, NoneType)):
+ return str(d)
+ output = '\n"
+ return output
+
+
+def get_audio_urls(the_audio, question=None):
+ output = []
+ the_list = []
+ to_try = {}
+ for audio_item in the_audio:
+ if audio_item['type'] != 'audio':
+ continue
+ found_upload = False
+ pattern = re.compile(r'^\[FILE ([^,\]]+)')
+ for file_ref in re.findall(pattern, audio_item['text']):
+ found_upload = True
+ m = re.match(r'[0-9]+', file_ref)
+ if m:
+ file_info = file_finder(file_ref, question=question)
+ if 'path' in file_info:
+ if file_info['mimetype'] == 'audio/ogg':
+ output.append([url_finder(file_ref, _question=question), file_info['mimetype']])
+ elif os.path.isfile(file_info['path'] + '.ogg'):
+ output.append([url_finder(file_ref, ext='ogg', _question=question), 'audio/ogg'])
+ if file_info['mimetype'] == 'audio/mpeg':
+ output.append([url_finder(file_ref, _question=question), file_info['mimetype']])
+ elif os.path.isfile(file_info['path'] + '.mp3'):
+ output.append([url_finder(file_ref, ext='mp3', _question=question), 'audio/mpeg'])
+ if file_info['mimetype'] not in ['audio/mpeg', 'audio/ogg']:
+ output.append([url_finder(file_ref, _question=question), file_info['mimetype']])
+ else:
+ the_list.append({'text': file_ref, 'package': audio_item['package']})
+ if not found_upload:
+ the_list.append(audio_item)
+ for audio_item in the_list:
+ mimetype, encoding = mimetypes.guess_type(audio_item['text']) # pylint: disable=unused-variable
+ if re.search(r'^http', audio_item['text']):
+ output.append([audio_item['text'], mimetype])
+ continue
+ basename = os.path.splitext(audio_item['text'])[0]
+ ext = os.path.splitext(audio_item['text'])[1]
+ if mimetype not in to_try:
+ to_try[mimetype] = []
+ to_try[mimetype].append({'basename': basename, 'filename': audio_item['text'], 'ext': ext, 'package': audio_item['package']})
+ if 'audio/mpeg' in to_try and 'audio/ogg' not in to_try:
+ to_try['audio/ogg'] = []
+ for attempt in to_try['audio/mpeg']:
+ if attempt['ext'] == '.MP3':
+ to_try['audio/ogg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.OGG', 'ext': '.OGG', 'package': attempt['package']})
+ else:
+ to_try['audio/ogg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.ogg', 'ext': '.ogg', 'package': attempt['package']})
+ if 'audio/ogg' in to_try and 'audio/mpeg' not in to_try:
+ to_try['audio/mpeg'] = []
+ for attempt in to_try['audio/ogg']:
+ if attempt['ext'] == '.OGG':
+ to_try['audio/mpeg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.MP3', 'ext': '.MP3', 'package': attempt['package']})
+ else:
+ to_try['audio/mpeg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.mp3', 'ext': '.mp3', 'package': attempt['package']})
+ for mimetype in reversed(sorted(to_try.keys())):
+ for attempt in to_try[mimetype]:
+ parts = attempt['filename'].split(':')
+ if len(parts) < 2:
+ parts = [attempt['package'], attempt['filename']]
+ if parts[0] is None:
+ parts[0] = 'None'
+ parts[1] = re.sub(r'^data/static/', '', parts[1])
+ full_file = parts[0] + ':data/static/' + parts[1]
+ file_info = file_finder(full_file, question=question)
+ if 'fullpath' in file_info:
+ url = url_finder(full_file, _question=question)
+ output.append([url, mimetype])
+ return [item for item in output if item[0] is not None]
+
+
+def get_video_urls(the_video, question=None):
+ output = []
+ the_list = []
+ to_try = {}
+ for video_item in the_video:
+ if video_item['type'] != 'video':
+ continue
+ found_upload = False
+ if re.search(r'^\[(YOUTUBE|VIMEO)[0-9\:]* ', video_item['text']):
+ output.append(html_filter(video_item['text']))
+ continue
+ pattern = re.compile(r'^\[FILE ([^,\]]+)')
+ for file_ref in re.findall(pattern, video_item['text']):
+ found_upload = True
+ m = re.match(r'[0-9]+', file_ref)
+ if m:
+ file_info = file_finder(file_ref, question=question)
+ if 'path' in file_info:
+ if file_info['mimetype'] == 'video/ogg':
+ output.append([url_finder(file_ref, _question=question), file_info['mimetype']])
+ elif os.path.isfile(file_info['path'] + '.ogv'):
+ output.append([url_finder(file_ref, ext='ogv', _question=question), 'video/ogg'])
+ if file_info['mimetype'] == 'video/mp4':
+ output.append([url_finder(file_ref, _question=question), file_info['mimetype']])
+ elif os.path.isfile(file_info['path'] + '.mp4'):
+ output.append([url_finder(file_ref, ext='mp4', _question=question), 'video/mp4'])
+ if file_info['mimetype'] not in ['video/mp4', 'video/ogg']:
+ output.append([url_finder(file_ref, _question=question), file_info['mimetype']])
+ else:
+ the_list.append({'text': file_ref, 'package': video_item['package']})
+ if not found_upload:
+ the_list.append(video_item)
+ for video_item in the_list:
+ mimetype, encoding = mimetypes.guess_type(video_item['text']) # pylint: disable=unused-variable
+ if re.search(r'^http', video_item['text']):
+ output.append([video_item['text'], mimetype])
+ continue
+ basename = os.path.splitext(video_item['text'])[0]
+ ext = os.path.splitext(video_item['text'])[1]
+ if mimetype not in to_try:
+ to_try[mimetype] = []
+ to_try[mimetype].append({'basename': basename, 'filename': video_item['text'], 'ext': ext, 'package': video_item['package']})
+ if 'video/mp4' in to_try and 'video/ogg' not in to_try:
+ to_try['video/ogg'] = []
+ for attempt in to_try['video/mp4']:
+ if attempt['ext'] == '.MP4':
+ to_try['video/ogg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.OGV', 'ext': '.OGV', 'package': attempt['package']})
+ else:
+ to_try['video/ogg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.ogv', 'ext': '.ogv', 'package': attempt['package']})
+ if 'video/ogg' in to_try and 'video/mp4' not in to_try:
+ to_try['video/mp4'] = []
+ for attempt in to_try['video/ogg']:
+ if attempt['ext'] == '.OGV':
+ to_try['video/mp4'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.MP4', 'ext': '.MP4', 'package': attempt['package']})
+ else:
+ to_try['audio/mpeg'].append({'basename': attempt['basename'], 'filename': attempt['basename'] + '.mp4', 'ext': '.mp4', 'package': attempt['package']})
+ for mimetype in reversed(sorted(to_try.keys())):
+ for attempt in to_try[mimetype]:
+ parts = attempt['filename'].split(':')
+ if len(parts) < 2:
+ parts = [attempt['package'], attempt['filename']]
+ parts[1] = re.sub(r'^data/static/', '', parts[1])
+ if parts[0] is None:
+ full_file = 'data/static/' + parts[1]
+ else:
+ full_file = parts[0] + ':data/static/' + parts[1]
+ file_info = file_finder(full_file, question=question)
+ if 'fullpath' in file_info:
+ url = url_finder(full_file, _question=question)
+ if url is not None:
+ output.append([url, mimetype])
+ return output
+
+
+def process_target(text):
+ return re.sub(r'\[TARGET ([^\]]+)\]', target_html, text)
+
+
+def to_text(html_doc, terms, links):
+ output = ""
+ # logmessage("to_text: html doc is " + str(html_doc))
+ if not html_doc.startswith('<'):
+ html_doc = "" + html_doc + ""
+ soup = BeautifulSoup(html_doc, 'html.parser')
+ [s.extract() for s in soup(['style', 'script', '[document]', 'head', 'title', 'audio', 'video', 'pre', 'attribution'])] # pylint: disable=expression-not-assigned
+ [s.extract() for s in soup.find_all(hidden)] # pylint: disable=expression-not-assigned
+ [s.extract() for s in soup.find_all('div', {'class': 'dainvisible'})] # pylint: disable=expression-not-assigned
+ for s in soup.find_all(do_show):
+ if s.name in ['input', 'textarea', 'img'] and s.has_attr('alt'):
+ words = s.attrs['alt']
+ if s.has_attr('placeholder'):
+ words += ", " + s.attrs['placeholder']
+ else:
+ words = s.get_text()
+ words = re.sub(r'\n\s*', ' ', words, flags=re.DOTALL)
+ output += words + "\n"
+ for s in soup.find_all('a'):
+ if s.has_attr('class') and s.attrs['class'][0] == 'daterm' and s.has_attr('data-bs-content'):
+ terms[s.string] = s.attrs['data-bs-content']
+ elif s.has_attr('href'): # and (s.attrs['href'].startswith(url) or s.attrs['href'].startswith('?')):
+ # logmessage("Adding a link: " + s.attrs['href'])
+ links.append((s.attrs['href'], s.get_text()))
+ output = re.sub(br'\u201c'.decode('raw_unicode_escape'), '"', output)
+ output = re.sub(br'\u201d'.decode('raw_unicode_escape'), '"', output)
+ output = re.sub(br'\u2018'.decode('raw_unicode_escape'), "'", output)
+ output = re.sub(br'\u2019'.decode('raw_unicode_escape'), "'", output)
+ output = re.sub(br'\u201b'.decode('raw_unicode_escape'), "'", output)
+ output = re.sub(r'>', '>', output)
+ output = re.sub(r'<', '<', output)
+ output = re.sub(r'>', '>', output)
+ output = re.sub(r'<', '<', output)
+ output = re.sub(r'<[^>]+>', '', output)
+ output = re.sub(r'\n$', '', output)
+ output = re.sub(r' +', ' ', output)
+ return output
+
+bad_list = ['div', 'option']
+
+good_list = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'button', 'textarea', 'note']
+
+
+def do_show(element):
+ if re.match('', str(element), re.DOTALL):
+ return False
+ if element.name in ['option'] and element.has_attr('selected'):
+ return True
+ if element.name in bad_list:
+ return False
+ if element.name in ['img', 'input'] and element.has_attr('alt'):
+ return True
+ if element.name in good_list:
+ return True
+ if element.parent and element.parent.name in good_list:
+ return False
+ if element.string:
+ return True
+ if re.match(r'\s+', element.get_text()):
+ return False
+ return False
+
+
+def hidden(element):
+ if element.name == 'input':
+ if element.has_attr('type'):
+ if element.attrs['type'] == 'hidden':
+ return True
+ return False
diff --git a/docassemble_base/docassemble/base/filter/image_docx.py b/docassemble_base/docassemble/base/filter/image_docx.py
new file mode 100644
index 000000000..662a7bc34
--- /dev/null
+++ b/docassemble_base/docassemble/base/filter/image_docx.py
@@ -0,0 +1,56 @@
+import re
+from docxtpl import InlineImage
+from docx.shared import Mm, Inches, Pt, Cm, Twips
+from docassemble.base.hooks import file_finder
+from docassemble.base.filter.utils import convert_svg_to_png
+
+
+def fix_double_quote(the_string):
+ return '"' + re.sub('"', '"', the_string) + '"'
+
+
+class CustomInlineImage(InlineImage):
+ alt_text = None
+
+ def __init__(self, tpl, image_descriptor, width=None, height=None, anchor=None, alt_text=None):
+ super().__init__(tpl, image_descriptor, width=width, height=height, anchor=anchor)
+ self.alt_text = alt_text
+
+ def _insert_image(self):
+ output = super()._insert_image()
+ if self.alt_text:
+ return re.sub(' 0:
+ prior_values = formatting_stack.pop()
+ spacing_command = prior_values['spacing_command']
+ after_space = prior_values['after_space']
+ default_indentation = prior_values['default_indentation']
+ indentation_command = prior_values['indentation_command']
+ elif re.search(r'\[TIGHTSPACING\]', line):
+ spacing_command = rtf_spacing['tight']
+ default_spacing = 'tight'
+ after_space = after_space_multiplier * rtf_after_space[default_spacing]
+ default_indentation = False
+ elif re.search(r'\[SINGLESPACING\]', line):
+ spacing_command = rtf_spacing['single']
+ default_spacing = 'single'
+ after_space = after_space_multiplier * rtf_after_space[default_spacing]
+ default_indentation = False
+ elif re.search(r'\[ONEANDAHALFSPACING\]', line):
+ spacing_command = rtf_spacing['oneandahalf']
+ default_spacing = 'oneandahalf'
+ after_space = after_space_multiplier * rtf_after_space[default_spacing]
+ elif re.search(r'\[DOUBLESPACING\]', line):
+ spacing_command = rtf_spacing['double']
+ default_spacing = 'double'
+ after_space = after_space_multiplier * rtf_after_space[default_spacing]
+ elif re.search(r'\[TRIPLESPACING\]', line):
+ spacing_command = rtf_spacing['triple']
+ default_spacing = 'triple'
+ after_space = after_space_multiplier * rtf_after_space[default_spacing]
+ elif re.search(r'\[START_INDENTATION\]', line):
+ indentation_command = r'\\fi' + str(indentation_amount) + " "
+ elif re.search(r'\[STOP_INDENTATION\]', line):
+ indentation_command = r'\\fi0 '
+ elif line != '':
+ special_after_space = None
+ special_spacing = None
+ if re.search(r'\[BORDER\]', line):
+ line = re.sub(r' *\[BORDER\] *', r'', line)
+ border_text = r'\\box \\brdrhair \\brdrw1 \\brdrcf1 \\brsp29 '
+ else:
+ border_text = r''
+ line = re.sub(r'{(\\pard\\intbl \\q[lrc] \\f[0-9]+ \\sa[0-9]+ \\li[0-9]+ \\fi[0-9]+.*?)\\par}', r'\1', line)
+ if re.search(r'\[NOPAR\]', line):
+ line = re.sub(r'{\\pard \\ql \\f[0-9]+ \\sa[0-9]+ \\li[0-9]+ \\fi-?[0-9]* *(.*?)\\par}', r'\1', line)
+ line = re.sub(r' *\[NOPAR\] *', r'', line)
+ n = re.search(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+) *([0-9\.]+ *[A-Za-z]+)\]', line)
+ m = re.search(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+)\]', line)
+ if n:
+ line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
+ line = re.sub(r'\\ri-?[0-9]+ ', r'', line)
+ line = re.sub(r'\\li-?[0-9]+ ', r'\\li' + str(convert_length(n.group(1), 'twips')) + r' \\ri' + str(convert_length(n.group(2), 'twips')) + ' ', line)
+ line = re.sub(r'\[INDENTBY[^\]]*\]', '', line)
+ elif m:
+ line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
+ line = re.sub(r'\\li-?[0-9]+ ', r'\\li' + str(convert_length(m.group(1), 'twips')) + ' ', line)
+ line = re.sub(r' *\[INDENTBY[^\]]*\] *', '', line)
+ elif re.search(r'\[NOINDENT\]', line):
+ line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
+ line = re.sub(r' *\[NOINDENT\] *', '', line)
+ elif re.search(r'\[FLUSHLEFT\]', line):
+ line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
+ line = re.sub(r' *\[FLUSHLEFT\] *', '', line)
+ special_after_space = after_space_multiplier * 1
+ special_spacing = rtf_spacing['single']
+ elif re.search(r'\[FLUSHRIGHT\]', line):
+ line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
+ line = re.sub(r'\\ql', r'\\qr', line)
+ line = re.sub(r' *\[FLUSHRIGHT\] *', '', line)
+ special_after_space = after_space_multiplier * 1
+ special_spacing = rtf_spacing['single']
+ elif re.search(r'\[CENTER\]', line):
+ line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
+ line = re.sub(r'\\ql', r'\\qc', line)
+ line = re.sub(r' *\[CENTER\] *', '', line)
+ elif re.search(r'\[BOLDCENTER\]', line):
+ line = re.sub(r'\\fi-?[0-9]+ ', r'\\fi0 ', line)
+ line = re.sub(r'\\ql', r'\\qc \\b', line)
+ line = re.sub(r' *\[BOLDCENTER\] *', '', line)
+ elif indentation_command != '' and not re.search(r'\\widctlpar', line):
+ line = re.sub(r'\\fi-?[0-9]+ ', indentation_command, line)
+ if not re.search(r'\\s[0-9]', line):
+ if special_spacing:
+ spacing_command_to_use = special_spacing
+ else:
+ spacing_command_to_use = spacing_command
+ line = re.sub(r'\\pard ', r'\\pard ' + str(spacing_command_to_use) + str(border_text), line)
+ line = re.sub(r'\\pard\\intbl ', r'\\pard\\intbl ' + str(spacing_command_to_use) + str(border_text), line)
+ if not (re.search(r'\\fi0\\(endash|bullet)', line) or re.search(r'\\s[0-9]', line) or re.search(r'\\intbl', line)):
+ if special_after_space:
+ after_space_to_use = special_after_space
+ else:
+ after_space_to_use = after_space
+ if after_space_to_use > 0:
+ line = re.sub(r'\\sa[0-9]+ ', r'\\sa' + str(after_space_to_use) + ' ', line)
+ else:
+ line = re.sub(r'\\sa[0-9]+ ', r'\\sa0 ', line)
+ text += line + '\n'
+ text = re.sub(r'{\\pard \\sl[0-9]+\\slmult[0-9]+ \\ql \\f[0-9]+ \\sa[0-9]+ \\li[0-9]+ \\fi-?[0-9]*\s*\\par}', r'', text)
+ text = re.sub(r'\[MANUALSKIP\]', r'{\\pard \\sl0 \\ql \\f0 \\sa0 \\li0 \\fi0 \\par}', text)
+ return text
+
+
+def metadata_filter(text, doc_format):
+ if doc_format == 'pdf':
+ text = re.sub(r'\*\*([^\*]+?)\*\*', r'\\begingroup\\bfseries \1\\endgroup {}', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\*([^\*]+?)\*', r'\\begingroup\\itshape \1\\endgroup {}', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\_\_([^\_]+?)\_\_', r'\\begingroup\\bfseries \1\\endgroup {}', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\_([^\_]+?)\_*', r'\\begingroup\\itshape \1\\endgroup {}', text, flags=re.MULTILINE | re.DOTALL)
+ return text
+
+
+def redact_latex(match):
+ return '\\redactword{' + str(escape_latex(match.group(1))) + '}'
+
+
+def pdf_filter(text, metadata=None, question=None):
+ if metadata is None:
+ metadata = {}
+ text = text + "\n\n"
+ text = re.sub(r'\[\[([^\]]*)\]\]', r'\1', text)
+ text = re.sub(r'\[EMOJI ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_string(x, emoji=True, question=question), text)
+ text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', lambda x: image_include_string(x, question=question), text)
+ text = re.sub(r'\[FILE ([^,\]]+), *([0-9A-Za-z.%]+)\]', lambda x: image_include_string(x, question=question), text)
+ text = re.sub(r'\[FILE ([^,\]]+)\]', lambda x: image_include_string(x, question=question), text)
+ text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+), *([^\]]*)\]', qr_include_string, text)
+ text = re.sub(r'\[QR ([^,\]]+), *([0-9A-Za-z.%]+)\]', qr_include_string, text)
+ text = re.sub(r'\[QR ([^\]]+)\]', qr_include_string, text)
+ text = re.sub(r'\[MAP ([^\]]+)\]', '', text)
+ text = replace_fields(text)
+ # text = re.sub(r'\[FIELD ([^\]]+)\]', '', text)
+ text = re.sub(r'\[TARGET ([^\]]+)\]', '', text)
+ text = re.sub(r'\[YOUTUBE[^ ]* ([^\]]+)\]', '', text)
+ text = re.sub(r'\[VIMEO[^ ]* ([^\]]+)\]', '', text)
+ text = re.sub(r'\$\$+', '$', text)
+ text = re.sub(r'\\clearpage *\\clearpage', r'\\clearpage', text)
+ text = re.sub(r'\[BORDER\]\s*\[(BEGIN_TWOCOL|BEGIN_CAPTION|TIGHTSPACING|SINGLESPACING|DOUBLESPACING|START_INDENTATION|STOP_INDENTATION|NOINDENT|FLUSHLEFT|FLUSHRIGHT|CENTER|BOLDCENTER|INDENTBY[^\]]*)\]', r'[\1] [BORDER]', text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[START_INDENTATION\]', r'\\setlength{\\parindent}{\\myindentamount}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
+ text = re.sub(r'\[STOP_INDENTATION\]', r'\\setlength{\\parindent}{0in}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
+ text = re.sub(r'\[BEGIN_CAPTION\](.+?)\[VERTICAL_LINE\]\s*(.+?)\[END_CAPTION\]', pdf_caption, text, flags=re.DOTALL)
+ text = re.sub(r'\[BEGIN_TWOCOL\](.+?)\[BREAK\]\s*(.+?)\[END_TWOCOL\]', pdf_two_col, text, flags=re.DOTALL)
+ text = re.sub(r'\[TIGHTSPACING\]\s*', r'\\singlespacing\\setlength{\\parskip}{0pt}\\setlength{\\parindent}{0pt}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
+ text = re.sub(r'\[SINGLESPACING\]\s*', r'\\singlespacing\\setlength{\\parskip}{\\myfontsize}\\setlength{\\parindent}{0pt}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
+ text = re.sub(r'\[DOUBLESPACING\]\s*', r'\\doublespacing\\setlength{\\parindent}{\\myindentamount}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
+ text = re.sub(r'\[ONEANDAHALFSPACING\]\s*', r'\\onehalfspacing\\setlength{\\parindent}{\\myindentamount}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
+ text = re.sub(r'\[TRIPLESPACING\]\s*', r'\\setlength{\\parindent}{\\myindentamount}\\setlength{\\RaggedRightParindent}{\\parindent}', text)
+ text = re.sub(r'\[NBSP\]', r'\\myshow{\\nonbreakingspace}', text)
+ text = re.sub(r'\[REDACTION_SPACE\]', r'\\redactword{~}\\hspace{0pt}', text)
+ text = re.sub(r'\[REDACTION_WORD ([^\]]+)\]', redact_latex, text)
+ text = re.sub(r'\[ENDASH\]', r'\\myshow{\\myendash}', text)
+ text = re.sub(r'\[EMDASH\]', r'\\myshow{\\myemdash}', text)
+ text = re.sub(r'\[HYPHEN\]', r'\\myshow{\\myhyphen}', text)
+ text = re.sub(r'\[CHECKBOX\]', r'{\\rule{0.3in}{0.4pt}}', text)
+ text = re.sub(r'\[BLANK\]', r'\\leavevmode{\\xrfill[-2pt]{0.4pt}}', text)
+ text = re.sub(r'\[BLANKFILL\]', r'\\leavevmode{\\xrfill[-2pt]{0.4pt}}', text)
+ text = re.sub(r'\[PAGEBREAK\]\s*', r'\\clearpage ', text)
+ text = re.sub(r'\[PAGENUM\]', r'\\myshow{\\thepage\\myxspace}', text)
+ text = re.sub(r'\[TOTALPAGES\]', r'\\myshow{\\pageref*{LastPage}\\myxspace}', text)
+ text = re.sub(r'\[SECTIONNUM\]', r'\\myshow{\\thesection\\myxspace}', text)
+ text = re.sub(r'\[VERTICALSPACE\] *', r'\\rule[-24pt]{0pt}{0pt}', text)
+ text = re.sub(r'\[NEWLINE\] *', r'\\newline ', text)
+ text = re.sub(r'\[NEWPAR\] *', r'\\par ', text)
+ text = re.sub(r'\[BR\] *', r'\\manuallinebreak ', text)
+ text = re.sub(r'\[TAB\] *', r'\\manualindent ', text)
+ text = re.sub(r' *\[END\] *', r'\n', text)
+ text = re.sub(r'\[NOINDENT\] *', r'\\noindent ', text)
+ text = re.sub(r'\[FLUSHLEFT\] *(.+?)\n *\n', flushleft_pdf, text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[FLUSHRIGHT\] *(.+?)\n *\n', flushright_pdf, text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[CENTER\] *(.+?)\n *\n', center_pdf, text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[BOLDCENTER\] *(.+?)\n *\n', boldcenter_pdf, text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+)\] *(.+?)\n *\n', indentby_left_pdf, text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[INDENTBY *([0-9\.]+ *[A-Za-z]+) *([0-9]+ *[A-Za-z]+)\] *(.+?)\n *\n', indentby_both_pdf, text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\[BORDER\] *(.+?)\n *\n', border_pdf, text, flags=re.MULTILINE | re.DOTALL)
+ text = re.sub(r'\s*\[SKIPLINE\]\s*', r'\\par\\myskipline ', text)
+ return text
+
+
+def clean_markdown_to_latex(string):
+ string = re.sub(r'\s*\[SKIPLINE\]\s*', r'\\par\\myskipline ', string)
+ string = re.sub(r'^[\n ]+', '', string)
+ string = re.sub(r'[\n ]+$', '', string)
+ string = re.sub(r' *\n *$', '\n', string)
+ string = re.sub(r'\n{2,}', '[NEWLINE]', string)
+ string = re.sub(r'\[BR\]', '[NEWLINE]', string)
+ string = re.sub(r'\[(NOINDENT|FLUSHLEFT|FLUSHRIGHT|CENTER|BOLDCENTER|TIGHTSPACING|SINGLESPACING|DOUBLESPACING|START_INDENTATION|STOP_INDENTATION|PAGEBREAK)\]\s*', '', string)
+ string = re.sub(r'\*\*([^\*]+?)\*\*', r'\\textbf{\1}', string)
+ string = re.sub(r'\*([^\*]+?)\*', r'\\emph{\1}', string)
+ string = re.sub(r'(? 0 and file_info['width'] > 0:
+ scale = float(pixels)/float(file_info['width'])
+ # logmessage("scale is " + str(scale))
+ if scale*float(file_info['height']) > float(MAX_HEIGHT_POINTS):
+ scale = float(MAX_HEIGHT_POINTS)/float(file_info['height'])
+ # logmessage("scale is " + str(scale))
+ if scale*float(file_info['width']) > float(MAX_WIDTH_POINTS):
+ scale = float(MAX_WIDTH_POINTS)/float(file_info['width'])
+ # logmessage("scale is " + str(scale))
+ # scale *= 100.0
+ # logmessage("scale is " + str(scale))
+ # scale = int(scale)
+ # logmessage("scale is " + str(scale))
+ wtwips = int(scale*float(file_info['width'])*20.0)
+ htwips = int(scale*float(file_info['height'])*20.0)
+ image = Image(file_info['fullpath'])
+ image.Data = re.sub(r'\\picwgoal([0-9]+)', r'\\picwgoal' + str(wtwips), image.Data)
+ image.Data = re.sub(r'\\pichgoal([0-9]+)', r'\\pichgoal' + str(htwips), image.Data)
+ else:
+ image = Image(file_info['fullpath'])
+ if insert_page_breaks:
+ content = '\\page '
+ else:
+ content = ''
+ # logmessage(content + image.Data)
+ return content + image.Data
+
+
+def convert_percent(match):
+ percentage = match.group(1)
+ return str(float(percentage)/100.0) + '\\textwidth'
+
+
+def image_include_string(match, emoji=False, question=None):
+ file_reference = match.group(1)
+ if question and file_reference in question.interview.images:
+ file_reference = question.interview.images[file_reference].get_reference()
+ try:
+ width = match.group(2)
+ assert width != 'None'
+ width = re.sub(r'^(.*)px', convert_pixels, width)
+ width = re.sub(r'^(.*)%', convert_percent, width)
+ if width == "full":
+ width = '\\textwidth'
+ except:
+ width = get_default_image_width()
+ if match.lastindex == 3:
+ alt_text = match.group(3)
+ else:
+ alt_text = None
+ file_info = file_finder(file_reference, question=question)
+ if 'path' in file_info and 'extension' in file_info:
+ convert_svg_to_eps(file_info)
+ if file_info['extension'] == 'gif':
+ with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".png", delete=False) as png_file:
+ try:
+ with PIL.Image.open(file_info['fullpath']) as im:
+ im.save(png_file.name)
+ png_file.close()
+ file_info['path'] = png_file.name
+ file_info['fullpath'] = png_file.name
+ file_info['extension'] = 'png'
+ file_info['mimetype'] = 'image/png'
+ except BaseException as err:
+ logmessage("Could not convert GIF to PNG: " + err.__class__.__name__ + ": " + str(err))
+ if 'mimetype' in file_info and file_info['mimetype']:
+ if re.search(r'^(audio|video)', file_info['mimetype']):
+ return '[reference to file type that cannot be displayed]'
+ if 'path' in file_info:
+ if 'extension' in file_info:
+ if file_info['extension'] in ['png', 'jpg', 'pdf', 'eps', 'jpe', 'jpeg', 'docx', 'rtf', 'doc', 'odt']:
+ if file_info['extension'] == 'pdf':
+ output = '\\includepdf[pages={-}]{' + file_info['path'] + '.pdf}'
+ elif file_info['extension'] in ('docx', 'rtf', 'doc', 'odt'):
+ if not os.path.isfile(file_info['path'] + '.pdf'):
+ fg_make_pdf_for_word_path(file_info['path'], file_info['extension'])
+ output = '\\includepdf[pages={-}]{' + file_info['path'] + '.pdf}'
+ else:
+ if alt_text:
+ alt_text_string = ', alt={' + re.sub(r'[{}]', '', alt_text) + '}'
+ else:
+ alt_text_string = ''
+ if emoji:
+ output = '\\raisebox{-.6\\dp\\strutbox}{\\mbox{\\includegraphics[width=' + width + alt_text_string + ']{' + file_info['path'] + '}}}'
+ else:
+ output = '\\mbox{\\includegraphics[width=' + width + alt_text_string + ']{' + file_info['path'] + '}}'
+ if width == '\\textwidth':
+ output = '\\clearpage ' + output + '\\clearpage '
+ return output
+ return '[invalid graphics reference]'
+
+
+def qr_include_string(match):
+ string = match.group(1)
+ try:
+ width = match.group(2)
+ assert width != 'None'
+ width = re.sub(r'^(.*)px', convert_pixels, width)
+ if width == "full":
+ width = '\\textwidth'
+ except:
+ width = get_default_image_width()
+ if match.lastindex == 3:
+ alt_text = match.group(3)
+ else:
+ alt_text = None
+ im = qrcode.make(string)
+ with tempfile.NamedTemporaryFile(prefix="datemp", suffix=".png", delete=False) as the_image:
+ # this_thread.temporary_resources.add(the_image.name)
+ im.save(the_image.name)
+ if alt_text:
+ alt_text_string = ', alt={' + re.sub(r'[{}]', '', alt_text) + '}'
+ else:
+ alt_text_string = ''
+ output = '\\mbox{\\includegraphics[width=' + width + alt_text_string + ']{' + the_image.name + '}}'
+ if width == '\\textwidth':
+ output = '\\clearpage ' + output + '\\clearpage '
+ # logmessage("Output is " + output)
+ return output
+
+
+def rtf_caption_table(match):
+ table_text = """\\trowd \\irow0\\irowband0\\lastrow \\ltrrow\\ts24\\trgaph108\\trleft0\\trbrdrt\\brdrs\\brdrw10 \\trbrdrl\\brdrs\\brdrw10 \\trbrdrb\\brdrs\\brdrw10 \\trbrdrr\\brdrs\\brdrw10 \\trbrdrh\\brdrs\\brdrw10 \\trbrdrv\\brdrs\\brdrw10
+\\trftsWidth1\\trftsWidthB3\\trftsWidthA3\\trautofit1\\trpaddl108\\trpaddr108\\trpaddfl3\\trpaddft3\\trpaddfb3\\trpaddfr3\\trcbpat1\\trcfpat1\\tblrsid1508006\\tbllkhdrrows\\tbllkhdrcols\\tbllknocolband\\tblind0\\tblindtype3 \\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone
+\\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrs\\brdrw10 \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx4680\\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrs\\brdrw10 \\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx9468\\pard\\plain \\ltrpar
+\\ql \\li0\\ri0\\widctlpar\\intbl\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\pararsid1508006\\yts24 \\rtlch\\fcs1 \\af0\\afs22\\alang1025 \\ltrch\\fcs0 \\fs22\\lang1033\\langfe1033\\cgrid\\langnp1033\\langfenp1033 { [SAVE][TIGHTSPACING][STOP_INDENTATION]""" + match.group(1) + """}{\\cell}{""" + match.group(2) + """[RESTORE]}{\\cell}\\pard\\plain \\ltrpar
+\\ql \\li0\\ri0\\sa200\\sl276\\slmult1\\widctlpar\\intbl\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0 \\rtlch\\fcs1 \\af0\\afs22\\alang1025 \\ltrch\\fcs0 \\fs24\\lang1033\\langfe1033\\cgrid\\langnp1033\\langfenp1033 {\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\insrsid10753242
+\\trowd \\irow0\\irowband0\\lastrow \\ltrrow\\ts24\\trgaph108\\trleft0\\trbrdrt\\brdrs\\brdrw10 \\trbrdrl\\brdrs\\brdrw10 \\trbrdrb\\brdrs\\brdrw10 \\trbrdrr\\brdrs\\brdrw10 \\trbrdrh\\brdrs\\brdrw10 \\trbrdrv\\brdrs\\brdrw10
+\\trftsWidth1\\trftsWidthB3\\trftsWidthA3\\trautofit1\\trpaddl108\\trpaddr108\\trpaddfl3\\trpaddft3\\trpaddfb3\\trpaddfr3\\trcbpat1\\trcfpat1\\tblrsid1508006\\tbllkhdrrows\\tbllkhdrcols\\tbllknocolband\\tblind0\\tblindtype3 \\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone
+\\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrs\\brdrw10 \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx4680\\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrs\\brdrw10 \\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx9468\\row }"""
+ table_text += """\\pard \\ltrpar
+\\qc \\li0\\ri0\\sb0\\sl240\\slmult1\\widctlpar\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\itap0\\pararsid10753242"""
+ table_text = re.sub(r'\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0', r'\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\sl240 \\slmult1', table_text)
+ return table_text + '[MANUALSKIP]'
+
+
+def rtf_two_col(match):
+ table_text = """\\trowd \\irow0\\irowband0\\lastrow \\ltrrow\\ts24\\trgaph108\\trleft0\\trbrdrt\\brdrs\\brdrw10 \\trbrdrl\\brdrs\\brdrw10 \\trbrdrb\\brdrs\\brdrw10 \\trbrdrr\\brdrs\\brdrw10 \\trbrdrh\\brdrs\\brdrw10 \\trbrdrv\\brdrs\\brdrw10
+\\trftsWidth1\\trftsWidthB3\\trftsWidthA3\\trautofit1\\trpaddl108\\trpaddr108\\trpaddfl3\\trpaddft3\\trpaddfb3\\trpaddfr3\\trcbpat1\\trcfpat1\\tblrsid1508006\\tbllkhdrrows\\tbllkhdrcols\\tbllknocolband\\tblind0\\tblindtype3 \\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone
+\\clbrdrb\\brdrnone \\clbrdrr\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx4680\\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone \\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx9468\\pard\\plain \\ltrpar
+\\ql \\li0\\ri0\\widctlpar\\intbl\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\pararsid1508006\\yts24 \\rtlch\\fcs1 \\af0\\afs22\\alang1025 \\ltrch\\fcs0 \\fs22\\lang1033\\langfe1033\\cgrid\\langnp1033\\langfenp1033 {\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\insrsid2427490 [SAVE][TIGHTSPACING][STOP_INDENTATION]""" + match.group(1) + """}{\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\insrsid10753242\\charrsid2427490 \\cell}{""" + match.group(2) + """[RESTORE]}{\\cell}\\pard\\plain \\ltrpar
+\\ql \\li0\\ri0\\sa200\\sl276\\slmult1\\widctlpar\\intbl\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0 \\rtlch\\fcs1 \\af0\\afs22\\alang1025 \\ltrch\\fcs0 \\fs24\\lang1033\\langfe1033\\cgrid\\langnp1033\\langfenp1033 {\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\insrsid10753242
+\\trowd \\irow0\\irowband0\\lastrow \\ltrrow\\ts24\\trgaph108\\trleft0\\trbrdrt\\brdrs\\brdrw10 \\trbrdrl\\brdrs\\brdrw10 \\trbrdrb\\brdrs\\brdrw10 \\trbrdrr\\brdrs\\brdrw10 \\trbrdrh\\brdrs\\brdrw10 \\trbrdrv\\brdrs\\brdrw10
+\\trftsWidth1\\trftsWidthB3\\trftsWidthA3\\trautofit1\\trpaddl108\\trpaddr108\\trpaddfl3\\trpaddft3\\trpaddfb3\\trpaddfr3\\trcbpat1\\trcfpat1\\tblrsid1508006\\tbllkhdrrows\\tbllkhdrcols\\tbllknocolband\\tblind0\\tblindtype3 \\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone
+\\clbrdrb\\brdrnone \\clbrdrr\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx4680\\clvertalc\\clbrdrt\\brdrnone \\clbrdrl\\brdrnone \\clbrdrb\\brdrnone \\clbrdrr\\clshdng0\\brdrnone \\cltxlrtb\\clftsWidth3\\clwWidth4732 \\cellx9468\\row }"""
+ table_text += """\\pard \\ltrpar
+\\qc \\li0\\ri0\\sb0\\sl240\\slmult1\\widctlpar\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\itap0\\pararsid10753242"""
+ table_text = re.sub(r'\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0', r'\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\sl240 \\slmult1', table_text)
+ return table_text + '[MANUALSKIP]'
diff --git a/docassemble_base/docassemble/base/filter/utils.py b/docassemble_base/docassemble/base/filter/utils.py
new file mode 100644
index 000000000..501b67e5e
--- /dev/null
+++ b/docassemble_base/docassemble/base/filter/utils.py
@@ -0,0 +1,131 @@
+import re
+import tempfile
+import PIL
+from cairosvg import svg2png, svg2eps
+from ..logger import logmessage
+
+zerowidth = '\u200B' # pylint: disable=invalid-name
+
+list_types = ['1', 'A', 'a', 'I', 'i']
+
+DEFAULT_IMAGE_WIDTH = '4in'
+
+
+def set_default_image_width(width):
+ global DEFAULT_IMAGE_WIDTH
+ DEFAULT_IMAGE_WIDTH = str(width)
+
+
+def get_default_image_width():
+ return DEFAULT_IMAGE_WIDTH
+
+unit_multipliers = {'twips': 0.0500, 'hp': 0.5, 'in': 72, 'pt': 1, 'px': 1, 'em': 12, 'cm': 28.346472}
+
+
+def pixels_in(length):
+ m = re.search(r"([0-9.]+) *([a-z]+)", str(length).lower())
+ if m:
+ value = float(m.group(1))
+ unit = m.group(2)
+ # logmessage("value is " + str(value) + " and unit is " + unit)
+ if unit in unit_multipliers:
+ size = float(unit_multipliers[unit]) * value
+ # logmessage("size is " + str(size))
+ return int(size)
+ logmessage("Could not read " + str(length))
+ return 300
+
+
+def convert_length(length, unit):
+ value = pixels_in(length)
+ if unit in unit_multipliers:
+ size = float(value)/float(unit_multipliers[unit])
+ return int(size)
+ logmessage("Unit " + str(unit) + " is not a valid unit")
+ return 300
+
+
+def replace_fields(string, status=None, embedder=None):
+ if not re.search(r'\[FIELD ', string):
+ return string
+ matches = []
+ in_match = False
+ start_match = None
+ depth = 0
+ i = 0
+ while i < len(string):
+ if string[i:i+7] == '[FIELD ':
+ in_match = True
+ start_match = i
+ i += 7
+ continue
+ if in_match:
+ if string[i] == '[':
+ depth += 1
+ elif string[i] == ']':
+ if depth == 0:
+ i += 1
+ matches.append((start_match, i))
+ in_match = False
+ continue
+ depth -= 1
+ i += 1
+
+ field_strings = []
+ for (start, end) in matches:
+ field_strings.append(string[start:end])
+ # logmessage(repr(field_strings))
+ for field_string in field_strings:
+ if embedder is None:
+ string = string.replace(field_string, 'ERROR: FIELD cannot be used here')
+ else:
+ string = string.replace(field_string, embedder(status, field_string))
+ return string
+
+
+def repeat_along(chars, match):
+ output = chars * len(match.group(1))
+ # logmessage("Output is " + repr(output))
+ return output
+
+
+def convert_pixels(match):
+ pixels = match.group(1)
+ return str(int(pixels)/72.0) + "in"
+
+
+def convert_svg_to_eps(file_info):
+ try:
+ if file_info['extension'] == 'svg':
+ with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".eps", delete=False) as eps_file:
+ with open(file_info['fullpath'], 'rb') as fp:
+ svg2eps(file_obj=fp, write_to=eps_file)
+ file_info['path'] = eps_file.name
+ file_info['fullpath'] = eps_file.name
+ file_info['extension'] = 'eps'
+ file_info['mimetype'] = 'application/postscript'
+ eps_file.close()
+ except BaseException as err:
+ logmessage("Failure to convert SVG to EPS: " + err.__class__.__name__ + ": " + str(err))
+
+
+def convert_svg_to_png(file_info):
+ try:
+ if file_info['extension'] == 'svg':
+ with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".png", delete=False) as png_file:
+ with open(file_info['fullpath'], 'rb') as fp:
+ svg2png(file_obj=fp, write_to=png_file, dpi=300)
+ png_file.flush()
+ with PIL.Image.open(png_file.name) as im:
+ file_info['width'], file_info['height'] = im.size
+ file_info['path'] = png_file.name
+ file_info['fullpath'] = png_file.name
+ file_info['extension'] = 'png'
+ file_info['mimetype'] = 'image/png'
+ png_file.close()
+ except BaseException as err:
+ logmessage("Failure to convert SVG to PNG: " + err.__class__.__name__ + ": " + str(err))
+
+
+def sanitize_xml(text):
+ return re.sub(r'{([{%#])', '{' + zerowidth + r'\1', re.sub(r'([}%#])}', r'\1' + zerowidth + '}', text))
diff --git a/docassemble_base/docassemble/base/functions.py b/docassemble_base/docassemble/base/functions.py
index 841b7486c..b9007472e 100644
--- a/docassemble_base/docassemble/base/functions.py
+++ b/docassemble_base/docassemble/base/functions.py
@@ -1,5 +1,11 @@
+# ruff: noqa: F401
+# pylint: disable=unused-import
+# mypy: disable-error-code="var-annotated"
import re
+import sys
import types
+import traceback
+from types import SimpleNamespace
import os
import locale
import decimal
@@ -10,43 +16,172 @@
import json
import ast
import datetime
-import threading
import random
from collections.abc import Iterable
from unicodedata import normalize
from enum import Enum
from pathlib import Path
import importlib.resources
-import sys
import astunparse
-import tzlocal
import us
import pycountry
-import markdown
import ruamel.yaml
-from types import SimpleNamespace
-from docassemble.base.save_status import SS_NEW, SS_OVERWRITE, SS_IGNORE
-from docassemble.base.pattern import pattern_en, pattern_es, pattern_de, pattern_fr, pattern_it, pattern_nl
-
from pylatex.utils import escape_latex
# import operator
-import titlecase
from user_agents import parse as ua_parse
import phonenumbers
import werkzeug.utils
-import num2words
-from jinja2.runtime import Undefined
-from docassemble.base.logger import logmessage # pylint: disable=ungrouped-imports
-from docassemble.base.error import ForcedNameError, QuestionError, ResponseError, CommandError, BackgroundResponseError, BackgroundResponseActionError, ForcedReRun, DAError, DANameError, DAInvalidFilename
+from docassemble.base.background import bg_action
+from docassemble.base.error import (
+ ForcedNameError,
+ QuestionError,
+ ResponseError,
+ CommandError,
+ BackgroundResponseError,
+ BackgroundResponseActionError,
+ ForcedReRun,
+ DAError,
+ DANameError,
+ DAInvalidFilename,
+)
from docassemble.base.generate_key import random_string
+from docassemble.base.hooks import (
+ absolute_filename,
+ add_privilege,
+ chat_partners_available as server_chat_partners_available,
+ delete_record,
+ get_button_class_prefix,
+ get_chat_log as server_get_chat_log,
+ get_configuration,
+ get_debug_status,
+ get_default_timezone,
+ get_hostname,
+ get_login_url,
+ get_permissions_of_privilege,
+ get_privileges_list,
+ get_referer,
+ get_server_redis,
+ get_short_code,
+ get_url,
+ navigation_bar,
+ read_records,
+ release_lock,
+ remove_privilege,
+ retrieve_emails,
+ server_create_session,
+ server_create_user,
+ server_get_question_data,
+ server_get_secret,
+ server_get_session_variables,
+ server_get_user_info,
+ server_get_user_list,
+ server_go_back_in_session,
+ server_interview_menu,
+ server_invite_user,
+ server_run_action_in_session,
+ server_set_session_variables,
+ server_set_user_info,
+ transform_json_variables,
+ url_finder,
+ url_for,
+ user_interviews,
+ write_answer_json,
+ write_record,
+)
+from docassemble.base.language.capitalization import capitalize # noqa: F401 # pylint: disable=unused-import
+from docassemble.base.language.control import (
+ get_language,
+ set_language,
+ set_country,
+ get_country,
+ get_dialect,
+ get_voice,
+ set_locale,
+ get_locale,
+ update_locale,
+)
+from docassemble.base.language.core import (
+ language_functions,
+ ensure_definition,
+ language_function_constructor,
+ update_language_function,
+)
+from docassemble.base.language.currency import (
+ currency,
+ currency_symbol,
+ get_currency_symbol,
+)
+from docassemble.base.language.language import (
+ comma_list,
+ comma_and_list,
+ quantity_noun,
+ verb_past,
+ verb_present,
+ noun_plural,
+ noun_singular,
+ indefinite_article,
+ period_list,
+ name_suffix,
+ title_case,
+ add_separators,
+ its,
+ the,
+ her,
+ does_a_b,
+ was_a_b,
+ do_you,
+ this,
+ their,
+ possessify,
+ did_you,
+ salutation,
+ his,
+ a_in_the_b,
+ some,
+ these,
+ have_you,
+ your,
+ did_a_b,
+ possessify_long,
+ were_you,
+ is_word,
+ has_a_b,
+)
+from docassemble.base.language.numbers import (
+ number_to_word,
+ ordinal,
+ nice_number,
+ ordinal_functions,
+ ordinal_number,
+ update_nice_numbers,
+ update_ordinal_numbers,
+ update_ordinal_function,
+ string_to_number,
+)
+from docassemble.base.language.utils import fix_punctuation
+from docassemble.base.language.words import (
+ words,
+ word,
+ update_word_collection,
+ word_collection,
+)
+from docassemble.base.logger import logmessage
+from docassemble.base.save_status import SS_NEW, SS_OVERWRITE, SS_IGNORE
+from docassemble.base.thread_context import (
+ this_thread,
+ get_current_user_dict,
+ get_old_user_dict,
+)
import docassemble.base.astparser
+
+ordinal_function = ordinal
FileType = IOBase
equals_byte = bytes('=', 'utf-8')
TypeType = type(type(None))
locale.setlocale(locale.LC_ALL, '')
contains_volatile = re.compile(r'^(x\.|x\[|.*\[[ijklmn]\])')
match_brackets_or_dot = re.compile(r'(\[.+?\]|\.[a-zA-Z_][a-zA-Z0-9_]*)')
-python313 = sys.version_info >= (3, 13)
+# python313 = sys.version_info >= (3, 13)
__all__ = ['alpha', 'roman', 'item_label', 'ordinal', 'ordinal_number', 'comma_list', 'word', 'get_language', 'set_language', 'get_dialect', 'set_country', 'get_country', 'get_locale', 'set_locale', 'comma_and_list', 'need', 'nice_number', 'quantity_noun', 'currency_symbol', 'verb_past', 'verb_present', 'noun_plural', 'noun_singular', 'indefinite_article', 'capitalize', 'space_to_underscore', 'force_ask', 'period_list', 'name_suffix', 'currency', 'static_image', 'title_case', 'url_of', 'process_action', 'url_action', 'get_info', 'set_info', 'get_config', 'prevent_going_back', 'qr_code', 'action_menu_item', 'from_b64_json', 'defined', 'value', 'message', 'response', 'json_response', 'command', 'background_response', 'background_response_action', 'single_paragraph', 'quote_paragraphs', 'location_returned', 'location_known', 'user_lat_lon', 'interview_url', 'interview_url_action', 'interview_url_as_qr', 'interview_url_action_as_qr', 'interview_email', 'get_emails', 'action_arguments', 'action_argument', 'get_default_timezone', 'user_logged_in', 'user_privileges', 'user_has_privilege', 'user_info', 'current_context', 'background_action', 'background_response', 'background_response_action', 'us', 'set_live_help_status', 'chat_partners_available', 'phone_number_in_e164', 'phone_number_formatted', 'phone_number_is_valid', 'countries_list', 'country_name', 'write_record', 'read_records', 'delete_record', 'variables_as_json', 'all_variables', 'language_from_browser', 'device', 'plain', 'bold', 'italic', 'subdivision_type', 'indent', 'raw', 'fix_punctuation', 'set_progress', 'get_progress', 'referring_url', 'undefine', 'invalidate', 'dispatch', 'yesno', 'noyes', 'phone_number_part', 'log', 'encode_name', 'decode_name', 'interview_list', 'interview_menu', 'server_capabilities', 'session_tags', 'get_chat_log', 'get_user_list', 'get_user_info', 'set_user_info', 'get_user_secret', 'create_user', 'invite_user', 'create_session', 'get_session_variables', 'set_session_variables', 'go_back_in_session', 'manage_privileges', 'redact', 'forget_result_of', 're_run_logic', 'reconsider', 'get_question_data', 'set_save_status', 'single_to_double_newlines', 'verbatim', 'add_separators', 'store_variables_snapshot', 'update_terms', 'set_variables', 'language_name', 'run_action_in_session']
@@ -179,22 +314,22 @@ def wrap_up():
file_object.commit()
-def set_gathering_mode(mode, instanceName):
- # logmessage("set_gathering_mode: " + str(instanceName) + " with mode " + str(mode))
+def set_gathering_mode(mode, instance_name):
+ # logmessage("set_gathering_mode: " + str(instance_name) + " with mode " + str(mode))
if mode:
- if instanceName not in this_thread.gathering_mode:
+ if instance_name not in this_thread.gathering_mode:
# logmessage("set_gathering_mode: using " + str(get_current_variable()))
- this_thread.gathering_mode[instanceName] = get_current_variable()
+ this_thread.gathering_mode[instance_name] = get_current_variable()
else:
try:
- del this_thread.gathering_mode[instanceName]
+ del this_thread.gathering_mode[instance_name]
except KeyError:
pass
-def get_gathering_mode(instanceName):
- # logmessage("get_gathering_mode: " + str(instanceName))
- if instanceName not in this_thread.gathering_mode:
+def get_gathering_mode(instance_name):
+ # logmessage("get_gathering_mode: " + str(instance_name))
+ if instance_name not in this_thread.gathering_mode:
# logmessage("get_gathering_mode: returning False")
return False
# logmessage("get_gathering_mode: returning True")
@@ -208,9 +343,9 @@ def reset_gathering_mode(*pargs):
return
var = pargs[0]
todel = []
- for instanceName, curVar in this_thread.gathering_mode.items():
- if curVar == var:
- todel.append(instanceName)
+ for instance_name, current_var in this_thread.gathering_mode.items():
+ if current_var == var:
+ todel.append(instance_name)
# logmessage("reset_gathering_mode: deleting " + repr([y for y in todel]))
for item in todel:
try:
@@ -246,7 +381,7 @@ def get_chat_log(utc=False, timezone=None):
Returns:
list: A list of chat messages for the current interview session.
"""
- return server.get_chat_log(this_thread.current_info.get('yaml_filename', None), this_thread.current_info.get('session', None), this_thread.current_info.get('secret', None), utc=utc, timezone=timezone)
+ return server_get_chat_log(this_thread.current_info.get('yaml_filename', None), this_thread.current_info.get('session', None), this_thread.current_info.get('secret', None), utc=utc, timezone=timezone)
def get_current_package():
@@ -847,7 +982,7 @@ def user_has_privilege(*pargs):
return False
-class AttachmentInfo:
+class AttachmentInfo(SimpleNamespace):
pass
@@ -899,7 +1034,7 @@ def current_filename(self):
@property
def current_section(self):
try:
- return this_thread.current_section or get_user_dict()['nav'].current
+ return this_thread.current_section or get_current_user_dict()['nav'].current
except:
return None
@@ -913,7 +1048,7 @@ def inside_of(self):
@property
def request_url(self):
try:
- info = server.get_url()
+ info = get_url()
except:
info = {}
return info
@@ -1049,7 +1184,7 @@ def privileges(self):
def permissions(self):
enabled_privileges = set()
for privilege in user_privileges():
- enabled_privileges.update(server.get_permissions_of_privilege(privilege, privileged=True))
+ enabled_privileges.update(get_permissions_of_privilege(privilege, privileged=True))
return list(enabled_privileges)
@property
@@ -1106,7 +1241,7 @@ def current_filename(self):
def current_section(self):
warn_if_not_warned('user_info', 'current_section', 'current_context')
try:
- return this_thread.current_section or get_user_dict()['nav'].current
+ return this_thread.current_section or get_current_user_dict()['nav'].current
except:
return None
@@ -1272,7 +1407,7 @@ def chat_partners_available(*pargs, **kwargs):
if the_user_id == 'tNone':
logmessage("chat_partners_available: unable to get temporary user id")
return {'peer': 0, 'help': 0}
- return server.chat_partners_available(session_id, yaml_filename, the_user_id, mode, partner_roles)
+ return server_chat_partners_available(session_id, yaml_filename, the_user_id, mode, partner_roles)
def interview_email(key=None, index=None):
@@ -1293,8 +1428,8 @@ def interview_email(key=None, index=None):
"""
if key is None and index is not None:
raise DAError("interview_email: if you provide an index you must provide a key")
- domain = server.daconfig.get('incoming mail domain', server.daconfig.get('external hostname', server.hostname))
- return server.get_short_code(key=key, index=index) + '@' + domain
+ domain = get_configuration().get('incoming mail domain', get_configuration().get('external hostname', get_hostname()))
+ return get_short_code({"key": key, "index": index}) + '@' + domain
def get_emails(key=None, index=None):
@@ -1313,7 +1448,7 @@ def get_emails(key=None, index=None):
list: A list of objects representing e-mail addresses and their received
messages.
"""
- return server.retrieve_emails(key=key, index=index)
+ return retrieve_emails(key=key, index=index)
def modify_i_argument(args):
@@ -1384,7 +1519,7 @@ def interview_url(**kwargs):
is_new = False
url = None
if the_style == 'short':
- for k, v in server.daconfig.get('dispatch').items():
+ for k, v in get_configuration().get('dispatch').items():
if v == args['i']:
args['dispatch'] = k
del args['i']
@@ -1425,12 +1560,13 @@ def interview_url(**kwargs):
def temp_redirect(url, expire_seconds, do_local, one_time):
+ redis_server = get_server_redis()
while True:
code = random_string(32)
the_key = 'da:temporary_url:' + code
- if server.server_redis.get(the_key) is None:
+ if redis_server.get(the_key) is None:
break
- pipe = server.server_redis.pipeline()
+ pipe = redis_server.pipeline()
if one_time:
pipe.set(the_key, json.dumps({'url': url, 'once': True}))
else:
@@ -1438,8 +1574,8 @@ def temp_redirect(url, expire_seconds, do_local, one_time):
pipe.expire(the_key, expire_seconds)
pipe.execute()
if do_local:
- return server.url_for('run_temp', c=code)
- return server.url_for('run_temp', c=code, _external=True)
+ return url_for('main.run_temp', c=code)
+ return url_for('main.run_temp', c=code, _external=True)
def set_parts(**kwargs):
@@ -1680,7 +1816,7 @@ def interview_url_action(action, **kwargs):
pass
url = None
if the_style == 'short':
- for k, v in server.daconfig.get('dispatch').items():
+ for k, v in get_configuration().get('dispatch').items():
if v == args['i']:
args['dispatch'] = k
del args['i']
@@ -1886,7 +2022,7 @@ def set_save_status(status):
this_thread.misc['save_status'] = SS_OVERWRITE
if status == 'ignore':
this_thread.misc['save_status'] = SS_IGNORE
- server.release_lock(this_thread.current_info['session'], this_thread.current_info['yaml_filename'])
+ release_lock(this_thread.current_info['session'], this_thread.current_info['yaml_filename'])
class DANav:
@@ -2055,194 +2191,144 @@ def show_sections(self, style='inline', show_links=None):
if style == "inline":
the_class = 'danavlinks dainline'
interior_class = 'dainlineinside'
- a_class = "btn " + server.button_class_prefix + "secondary danavlink "
+ a_class = "btn " + get_button_class_prefix() + "secondary danavlink "
else:
if not self.visible():
return ''
the_class = 'danavlinks'
interior_class = None
a_class = None
- return ' ' + "\n" + server.navigation_bar(self, this_thread.interview, wrapper=False, inner_div_class=interior_class, a_class=a_class, show_links=show_links, show_nesting=False, include_arrows=True) + ' ' + "\n"
+ return ' ' + "\n" + navigation_bar(self, this_thread.interview, wrapper=False, inner_div_class=interior_class, a_class=a_class, show_links=show_links, show_nesting=False, include_arrows=True) + ' ' + "\n"
# word('This field is required.')
# word('Country Code')
# word('First Subdivision')
# word('Second Subdivision')
# word('Third Subdivision')
-word_collection = {
- 'en': {
- 'This field is required.': 'You need to fill this in.',
- "Country Code": 'Country Code (e.g., "us")',
- "First Subdivision": 'State Abbreviation (e.g., "NY")',
- "Second Subdivision": "County",
- "Third Subdivision": "Municipality",
- }
-}
-
-ordinal_numbers = {
-}
-
-nice_numbers = {
-}
-
-
-class WebFunc:
- pass
-server = WebFunc()
-
-
-def null_func(*pargs, **kwargs): # pylint: disable=unused-argument
- return None
-def null_func_dict(*pargs, **kwargs): # pylint: disable=unused-argument
- return {}
-
-
-def null_func_str(*pargs, **kwargs): # pylint: disable=unused-argument
- return ''
-
-
-def null_func_obj(*pargs, **kwargs): # pylint: disable=unused-argument
- return WebFunc()
-
-
-def null_func_func(*pargs, **kwargs): # pylint: disable=unused-argument
- return null_func
-
-server.SavedFile = null_func_obj
-server.absolute_filename = null_func
-server.add_privilege = null_func
-server.add_user_privilege = null_func
-server.alchemy_url = null_func_str
-server.connect_args = null_func_str
-server.applock = null_func
-server.bg_action = null_func
-server.ocr_google_in_background = null_func
-server.button_class_prefix = 'btn-'
-server.chat_partners_available = null_func
-server.chord = null_func_func
-server.create_user = null_func
-server.invite_user = null_func
-server.daconfig = {}
-server.debug = False
-server.debug_status = False
-server.default_country = 'US'
-server.default_dialect = 'us'
-server.default_voice = None
-server.default_language = 'en'
-server.default_locale = 'US.utf8'
-try:
- server.default_timezone = tzlocal.get_localzone_name()
-except:
- server.default_timezone = 'America/New_York'
-server.delete_answer_json = null_func
-server.delete_record = null_func
-server.fg_make_pdf_for_word_path = null_func
-server.fg_make_png_for_pdf = null_func
-server.fg_make_png_for_pdf_path = null_func
-server.file_finder = null_func_dict
-server.file_number_finder = null_func_dict
-server.file_privilege_access = null_func
-server.file_set_attributes = null_func
-server.file_user_access = null_func
-server.fix_pickle_obj = null_func_dict
-server.generate_csrf = null_func
-server.get_chat_log = null_func
-server.get_ext_and_mimetype = null_func
-server.get_new_file_number = null_func
-server.get_privileges_list = null_func
-server.get_question_data = null_func
-server.get_secret = null_func
-server.get_session_variables = null_func
-server.get_short_code = null_func
-server.get_sms_session = null_func_dict
-server.get_user_info = null_func
-server.get_user_list = null_func
-server.get_user_object = null_func_obj
-server.go_back_in_session = null_func
-server.hostname = 'localhost'
-server.initiate_sms_session = null_func
-server.interview_menu = null_func
-server.main_page_parts = {}
-server.make_png_for_pdf = null_func
-server.make_user_inactive = null_func
-server.navigation_bar = null_func
-server.ocr_finalize = null_func
-server.ocr_page = null_func
-server.path_from_reference = null_func_str
-server.read_answer_json = null_func
-server.read_records = null_func
-server.remove_privilege = null_func
-server.remove_user_privilege = null_func
-server.retrieve_emails = null_func
-server.save_numbered_file = null_func
-server.send_fax = null_func
-server.send_mail = null_func
-server.server_redis = None
-server.server_redis_user = None
-server.server_sql_defined = null_func
-server.server_sql_delete = null_func
-server.server_sql_get = null_func
-server.server_sql_keys = null_func
-server.server_sql_set = null_func
-server.create_session = null_func
-server.set_session_variables = null_func
-server.set_user_info = null_func
-server.sms_body = null_func_dict
-server.task_ready = null_func
-server.terminate_sms_session = null_func
-server.twilio_config = {}
-server.url_finder = null_func_dict
-server.url_for = null_func
-server.user_id_dict = null_func
-server.user_interviews = null_func
-server.variables_snapshot_connection = null_func
-server.wait_for_task = null_func
-server.worker_convert = null_func
-server.write_answer_json = null_func
-server.write_record = null_func
-server.to_text = null_func_str
-server.transform_json_variables = null_func
-server.get_login_url = null_func_dict
-server.run_action_in_session = null_func_dict
-server.invite_user = null_func
-server.get_url = null_func_dict
-
-
-def write_record(key, data):
- """Store data in the SQL database under the given key.
-
- Args:
- key (str): A string key to associate with the record.
- data: The data to store. Must be pickleable.
+# class WebFunc:
+# pass
+# server = WebFunc()
- Returns:
- int: The unique integer ID of the saved record.
- """
- return server.write_record(key, data)
-
-
-def read_records(key):
- """Return all records stored under the given key.
- Args:
- key (str): The string key used when calling ``write_record()``.
+# def null_func(*pargs, **kwargs): # pylint: disable=unused-argument
+# return None
- Returns:
- dict: A dictionary mapping unique integer record IDs to the stored data.
- """
- return server.read_records(key)
+# def null_func_dict(*pargs, **kwargs): # pylint: disable=unused-argument
+# return {}
-def delete_record(key, the_id):
- """Delete a record from the SQL database by key and ID.
- Args:
- key (str): The string key associated with the record.
- the_id (int): The unique integer ID of the record to delete.
- """
- return server.delete_record(key, the_id)
+# def null_func_str(*pargs, **kwargs): # pylint: disable=unused-argument
+# return ''
+
+
+# def null_func_obj(*pargs, **kwargs): # pylint: disable=unused-argument
+# return WebFunc()
+
+
+# def null_func_func(*pargs, **kwargs): # pylint: disable=unused-argument
+# return null_func
+
+# server.SavedFile = null_func_obj
+# server.absolute_filename = null_func
+# server.add_privilege = null_func
+# server.add_user_privilege = null_func
+# server.alchemy_url = null_func_str
+# server.connect_args = null_func_str
+# server.applock = null_func
+# server.bg_action = null_func
+# server.ocr_google_in_background = null_func
+# server.button_class_prefix = 'btn-'
+# server.chat_partners_available = null_func
+# server.chord = null_func_func
+# server.create_user = null_func
+# server.invite_user = null_func
+# server.daconfig = {}
+# server.debug = False
+# server.debug_status = False
+# server.default_country = 'US'
+# server.default_dialect = 'us'
+# server.default_voice = None
+# server.default_language = 'en'
+# server.default_locale = 'US.utf8'
+# try:
+# server.default_timezone = tzlocal.get_localzone_name()
+# except:
+# server.default_timezone = 'America/New_York'
+# server.delete_answer_json = null_func
+# server.delete_record = null_func
+# server.fg_make_pdf_for_word_path = null_func
+# server.fg_make_png_for_pdf = null_func
+# server.fg_make_png_for_pdf_path = null_func
+# server.file_finder = null_func_dict
+# server.file_number_finder = null_func_dict
+# server.file_privilege_access = null_func
+# server.file_set_attributes = null_func
+# server.file_user_access = null_func
+# server.fix_pickle_obj = null_func_dict
+# server.generate_csrf = null_func
+# server.get_chat_log = null_func
+# server.get_ext_and_mimetype = null_func
+# server.get_new_file_number = null_func
+# server.get_privileges_list = null_func
+# server.get_question_data = null_func
+# server.get_secret = null_func
+# server.get_session_variables = null_func
+# server.get_short_code = null_func
+# server.get_sms_session = null_func_dict
+# server.get_user_info = null_func
+# server.get_user_list = null_func
+# server.get_user_object = null_func_obj
+# server.go_back_in_session = null_func
+# server.hostname = 'localhost'
+# server.initiate_sms_session = null_func
+# server.interview_menu = null_func
+# server.main_page_parts = {}
+# server.make_png_for_pdf = null_func
+# server.make_user_inactive = null_func
+# server.navigation_bar = null_func
+# server.ocr_finalize = null_func
+# server.ocr_page = null_func
+# server.path_from_reference = null_func_str
+# server.read_answer_json = null_func
+# server.read_records = null_func
+# server.remove_privilege = null_func
+# server.remove_user_privilege = null_func
+# server.retrieve_emails = null_func
+# server.save_numbered_file = null_func
+# server.send_fax = null_func
+# server.send_mail = null_func
+# server.server_redis = None
+# server.server_redis_user = None
+# server.server_sql_defined = null_func
+# server.server_sql_delete = null_func
+# server.server_sql_get = null_func
+# server.server_sql_keys = null_func
+# server.server_sql_set = null_func
+# server.create_session = null_func
+# server.set_session_variables = null_func
+# server.set_user_info = null_func
+# server.sms_body = null_func_dict
+# server.task_ready = null_func
+# server.terminate_sms_session = null_func
+# server.twilio_config = {}
+# server.url_finder = null_func_dict
+# server.url_for = null_func
+# server.user_id_dict = null_func
+# server.user_interviews = null_func
+# server.variables_snapshot_connection = null_func
+# server.wait_for_task = null_func
+# server.worker_convert = null_func
+# server.write_answer_json = null_func
+# server.write_record = null_func
+# server.to_text = null_func_str
+# server.transform_json_variables = null_func
+# server.get_login_url = null_func_dict
+# server.run_action_in_session = null_func_dict
+# server.invite_user = null_func
+# server.get_url = null_func_dict
def url_of(file_reference, **kwargs):
@@ -2289,7 +2375,7 @@ def url_of(file_reference, **kwargs):
for param in ('expire', 'url_args', 'next', 'i', 'session', 'resume_existing'):
if param in kwargs and kwargs[param] is not None:
info[param] = kwargs[param]
- result = server.get_login_url(**info)
+ result = get_login_url(**info)
if result['status'] == 'success':
return result['url']
raise DAError("url_of: " + result['message'])
@@ -2299,7 +2385,7 @@ def url_of(file_reference, **kwargs):
kwargs['_question'] = get_current_question()
if kwargs.get('attachment', False):
kwargs['_attachment'] = True
- return server.url_finder(file_reference, **kwargs)
+ return url_finder(file_reference, **kwargs)
def server_capabilities():
@@ -2315,20 +2401,20 @@ def server_capabilities():
dict: A dictionary mapping capability names to True/False values.
"""
result = {'sms': False, 'fax': False, 'google_login': False, 'facebook_login': False, 'auth0_login': False, 'keycloak_login': False, 'authentik_login': False, 'azure_login': False, 'miniorange_login': False, 'phone_login': False, 'voicerss': False, 's3': False, 'azure': False, 'github': False, 'pypi': False, 'googledrive': False, 'google_maps': False}
- if 'twilio' in server.daconfig and isinstance(server.daconfig['twilio'], (list, dict)):
- if isinstance(server.daconfig['twilio'], list):
- tconfigs = server.daconfig['twilio']
+ if 'twilio' in get_configuration() and isinstance(get_configuration()['twilio'], (list, dict)):
+ if isinstance(get_configuration()['twilio'], list):
+ tconfigs = get_configuration()['twilio']
else:
- tconfigs = [server.daconfig['twilio']]
+ tconfigs = [get_configuration()['twilio']]
for tconfig in tconfigs:
if 'enable' in tconfig and not tconfig['enable']:
continue
result['sms'] = True
if tconfig.get('fax', False):
result['fax'] = True
- if 'phone login' in server.daconfig:
+ if 'phone login' in get_configuration():
result['phone_login'] = True
- if 'oauth' in server.daconfig and isinstance(server.daconfig['oauth'], dict):
+ if 'oauth' in get_configuration() and isinstance(get_configuration()['oauth'], dict):
oauth_providers = [
('google', 'google_login'),
('facebook', 'facebook_login'),
@@ -2341,35 +2427,35 @@ def server_capabilities():
('github', 'github')
]
for provider, result_key in oauth_providers:
- if provider in server.daconfig['oauth'] and isinstance(server.daconfig['oauth'][provider], dict) and ('enable' not in server.daconfig['oauth'][provider] or server.daconfig['oauth'][provider]['enable']):
+ if provider in get_configuration()['oauth'] and isinstance(get_configuration()['oauth'][provider], dict) and ('enable' not in get_configuration()['oauth'][provider] or get_configuration()['oauth'][provider]['enable']):
result[result_key] = True
- if 'pypi' in server.daconfig and server.daconfig['pypi'] is True:
+ if 'pypi' in get_configuration() and get_configuration()['pypi'] is True:
result['pypi'] = True
- if 'google' in server.daconfig and isinstance(server.daconfig['google'], dict) and ('google maps api key' in server.daconfig['google'] or 'api key' in server.daconfig['google']):
+ if 'google' in get_configuration() and isinstance(get_configuration()['google'], dict) and ('google maps api key' in get_configuration()['google'] or 'api key' in get_configuration()['google']):
result['google_maps'] = True
for key in ['voicerss', 's3', 'azure']:
- if key in server.daconfig and isinstance(server.daconfig[key], dict):
- if not ('enable' in server.daconfig[key] and not server.daconfig[key]['enable']):
+ if key in get_configuration() and isinstance(get_configuration()[key], dict):
+ if not ('enable' in get_configuration()[key] and not get_configuration()[key]['enable']):
result[key] = True
return result
# def generate_csrf(*pargs, **kwargs):
-# return server.generate_csrf(*pargs, **kwargs)
+# return generate_csrf(*pargs, **kwargs)
# def chat_partners(*pargs, **kwargs):
# return dict(peer=0, help=0)
# def absolute_filename(*pargs, **kwargs):
-# return server.absolute_filename(*pargs, **kwargs)
+# return absolute_filename(*pargs, **kwargs)
-def update_server(**kwargs):
- for arg, func in kwargs.items():
- # logmessage("Setting " + str(arg))
- if arg == 'bg_action':
- def worker_wrapper(action, ui_notification, the_func=func, **kwargs):
- return worker_caller(the_func, ui_notification, {'action': action, 'arguments': kwargs})
- setattr(server, arg, worker_wrapper)
- else:
- setattr(server, arg, func)
+# def update_server(**kwargs):
+# for arg, func in kwargs.items():
+# # logmessage("Setting " + str(arg))
+# if arg == 'bg_action':
+# def worker_wrapper(action, ui_notification, the_func=func, **kwargs):
+# return worker_caller(the_func, ui_notification, {'action': action, 'arguments': kwargs})
+# setattr(server, arg, worker_wrapper)
+# else:
+# setattr(server, arg, func)
# the_write_record = basic_write_record
@@ -2447,10 +2533,10 @@ def __init__(self):
self.role = 'user'
# class ThreadVariables(threading.local):
-# language = server.default_language
-# dialect = server.default_dialect
-# country = server.default_country
-# locale = server.default_locale
+# language = get_default_language()
+# dialect = get_default_dialect()
+# country = get_default_country()
+# locale = get_default_locale()
# current_info = {}
# internal = {}
# # user_dict = None
@@ -2481,75 +2567,75 @@ def __init__(self):
# self.initialized = True
# self.__dict__.update(kw)
-this_thread = threading.local()
-
-def populate_this_thread_defaults():
- this_thread.language = server.default_language
- this_thread.dialect = server.default_dialect
- this_thread.voice = server.default_voice
- this_thread.country = server.default_country
- this_thread.locale = server.default_locale
- this_thread.current_info = {}
- this_thread.internal = {}
- this_thread.initialized = False
- this_thread.session_id = None
- this_thread.current_package = None
- this_thread.interview = None
- this_thread.interview_status = None
- this_thread.evaluation_context = None
- this_thread.gathering_mode = {}
- this_thread.global_vars = GenericObject()
- this_thread.current_variable = []
- this_thread.open_files = set()
- this_thread.markdown = markdown.Markdown(extensions=['smarty', 'markdown.extensions.sane_lists', 'markdown.extensions.tables', 'markdown.extensions.attr_list', 'markdown.extensions.md_in_html', 'footnotes'], output_format='html5')
- this_thread.saved_files = {}
- this_thread.message_log = []
- this_thread.misc = {}
- this_thread.probing = False
- this_thread.prevent_going_back = False
- this_thread.current_question = None
- this_thread.current_section = None
-
-populate_this_thread_defaults()
-
-
-def enable_threading():
- global this_thread
- this_thread = SimpleNamespace()
- populate_this_thread_defaults()
-
-
-def backup_thread_variables():
- reset_context()
- for key in ('pending_error', 'docx_subdocs', 'dbcache'):
- if key in this_thread.misc:
- del this_thread.misc[key]
- backup = {}
- for key in ('interview', 'interview_status', 'open_files', 'current_question'):
- if hasattr(this_thread, key):
- backup[key] = getattr(this_thread, key)
- for key in ['language', 'dialect', 'country', 'locale', 'current_info', 'internal', 'initialized', 'session_id', 'current_package', 'interview', 'interview_status', 'evaluation_context', 'gathering_mode', 'global_vars', 'current_variable', 'saved_files', 'message_log', 'misc', 'probing', 'prevent_going_back', 'current_question']:
- if hasattr(this_thread, key):
- backup[key] = getattr(this_thread, key)
- if key == 'global_vars':
- this_thread.global_vars = GenericObject()
- elif key == 'misc':
- for key in [item for item in this_thread.misc.keys() if item.startswith('yaml_')]:
- del this_thread.misc[key]
- setattr(this_thread, key, copy.deepcopy(this_thread.misc))
- elif key == 'current_info':
- setattr(this_thread, key, copy.deepcopy(getattr(this_thread, key)))
- elif key in ('internal', 'gathering_mode', 'saved_files'):
- setattr(this_thread, key, {})
- elif key in ('current_variable', 'message_log'):
- setattr(this_thread, key, [])
- return backup
-
-
-def restore_thread_variables(backup):
- # logmessage("restore_thread_variables")
- for key in list(backup.keys()):
- setattr(this_thread, key, backup[key])
+
+# exec with user_dict
+# docassemble_base/docassemble/base/parse.py
+# docassemble_base/docassemble/base/functions.py
+# docassemble_webapp/docassemble/webapp/tasks/worker_tasks.py
+# docassemble_webapp/docassemble/webapp/interview/helpers.py
+# docassemble_webapp/docassemble/webapp/interview/views.py
+# docassemble_webapp/docassemble/webapp/sms/views.py
+
+# reset_local_variables:
+# docassemble_base/docassemble/base/parse.py
+# - commented out
+# docassemble_base/docassemble/base/functions.py
+# - function definition
+# docassemble_webapp/docassemble/webapp/cron.py
+# - before running assemble
+# docassemble_webapp/docassemble/webapp/tasks/worker_tasks.py
+# - within each task
+# docassemble_webapp/docassemble/webapp/tasks/worker_common.py
+# - bg_context contextmanager
+# docassemble_webapp/docassemble/webapp/main/views.py
+# - before_request
+
+# backup_thread_variables:
+# docassemble_base/docassemble/base/functions.py
+# - function definition
+# docassemble_webapp/docassemble/webapp/interview/helpers.py
+# - get_session_variables
+# - go_back_in_session
+# - set_session_variables
+# - create_new_interview
+# - get_question_data
+# - run_action_in_session
+# docassemble_webapp/docassemble/webapp/sms/helpers.py
+# - sms_body, before calling do_sms. Called by send_sms_invite
+
+
+
+# def backup_thread_variables():
+# reset_context()
+# for key in ('pending_error', 'docx_subdocs', 'dbcache'):
+# if key in this_thread.misc:
+# del this_thread.misc[key]
+# backup = {}
+# for key in ('interview', 'interview_status', 'open_files', 'current_question'):
+# if hasattr(this_thread, key):
+# backup[key] = getattr(this_thread, key)
+# for key in ['language', 'dialect', 'country', 'locale', 'current_info', 'internal', 'initialized', 'session_id', 'current_package', 'interview', 'interview_status', 'evaluation_context', 'gathering_mode', 'global_vars', 'current_variable', 'saved_files', 'message_log', 'misc', 'probing', 'prevent_going_back', 'current_question']:
+# if hasattr(this_thread, key):
+# backup[key] = getattr(this_thread, key)
+# if key == 'global_vars':
+# this_thread.global_vars = GenericObject()
+# elif key == 'misc':
+# for key in [item for item in this_thread.misc.keys() if item.startswith('yaml_')]:
+# del this_thread.misc[key]
+# setattr(this_thread, key, copy.deepcopy(this_thread.misc))
+# elif key == 'current_info':
+# setattr(this_thread, key, copy.deepcopy(getattr(this_thread, key)))
+# elif key in ('internal', 'gathering_mode', 'saved_files'):
+# setattr(this_thread, key, {})
+# elif key in ('current_variable', 'message_log'):
+# setattr(this_thread, key, [])
+# return backup
+
+
+# def restore_thread_variables(backup):
+# # logmessage("restore_thread_variables")
+# for key in list(backup.keys()):
+# setattr(this_thread, key, backup[key])
def background_response(*pargs, **kwargs):
@@ -2619,69 +2705,19 @@ def background_action(*pargs, **kwargs):
ui_notification = pargs[1]
else:
ui_notification = None
- return server.bg_action(action, ui_notification, **kwargs)
-
-
-class BackgroundResult:
-
- def __init__(self, result):
- for attr in ('value', 'error_type', 'error_trace', 'error_message', 'variables'):
- if hasattr(result, attr):
- setattr(self, attr, getattr(result, attr))
- else:
- setattr(self, attr, None)
-
-
-class MyAsyncResult:
-
- def wait(self):
- if not hasattr(self, '_cached_result'):
- self._cached_result = BackgroundResult(server.worker_convert(self.obj).get())
- return True
-
- def failed(self):
- if not hasattr(self, '_cached_result'):
- self._cached_result = BackgroundResult(server.worker_convert(self.obj).get())
- if self._cached_result.error_type is not None:
- return True
- return False
-
- def ready(self):
- return server.worker_convert(self.obj).ready()
-
- def result(self):
- if not hasattr(self, '_cached_result'):
- self._cached_result = BackgroundResult(server.worker_convert(self.obj).get())
- return self._cached_result
-
- def get(self):
- if not hasattr(self, '_cached_result'):
- self._cached_result = BackgroundResult(server.worker_convert(self.obj).get())
- return self._cached_result.value
-
- def revoke(self, terminate=True):
- return server.worker_convert(self.obj).revoke(terminate=terminate)
+ return bg_action(action, ui_notification, **kwargs)
- def status(self):
- return server.worker_convert(self.obj).status
- def state(self):
- return server.worker_convert(self.obj).state
-
- def date_done(self):
- return server.worker_convert(self.obj).date_done
-
-
-def worker_caller(func, ui_notification, action):
- # logmessage("Got to worker_caller in functions")
- result = MyAsyncResult()
- result.obj = func.delay(this_thread.current_info['yaml_filename'], this_thread.current_info['user'], this_thread.current_info['session'], this_thread.current_info['secret'], this_thread.current_info['url'], this_thread.current_info['url_root'], action, extra=ui_notification)
- if ui_notification is not None:
- worker_key = 'da:worker:uid:' + str(this_thread.current_info['session']) + ':i:' + str(this_thread.current_info['yaml_filename']) + ':userid:' + str(this_thread.current_info['user']['the_user_id'])
- # logmessage("worker_caller: id is " + str(result.obj.id) + " and key is " + worker_key)
- server.server_redis.rpush(worker_key, result.obj.id)
- # logmessage("worker_caller: id is " + str(result.obj.id))
- return result
+# def worker_caller(func, ui_notification, action):
+# # logmessage("Got to worker_caller in functions")
+# result = MyAsyncResult()
+# result.obj = func.delay(this_thread.current_info['yaml_filename'], this_thread.current_info['user'], this_thread.current_info['session'], this_thread.current_info['secret'], this_thread.current_info['url'], this_thread.current_info['url_root'], action, extra=ui_notification)
+# if ui_notification is not None:
+# worker_key = 'da:worker:uid:' + str(this_thread.current_info['session']) + ':i:' + str(this_thread.current_info['yaml_filename']) + ':userid:' + str(this_thread.current_info['user']['the_user_id'])
+# # logmessage("worker_caller: id is " + str(result.obj.id) + " and key is " + worker_key)
+# get_server_redis().rpush(worker_key, result.obj.id)
+# # logmessage("worker_caller: id is " + str(result.obj.id))
+# return result
# def null_chat_partners(*pargs, **kwargs):
# return dict(peer=0, help=0)
@@ -2760,58 +2796,6 @@ def dump_to_bytes(self, *pargs, **kwargs):
altyamlstring = SafeYaml('yaml_altyamlstring')
-def ordinal_function_en(i, **kwargs):
- try:
- i = int(i)
- except:
- i = 0
- use_word = kwargs.get('use_word', None)
- if use_word is True:
- kwargs['function'] = 'ordinal'
- elif use_word is False:
- kwargs['function'] = 'ordinal_num'
- else:
- if i < 11:
- kwargs['function'] = 'ordinal'
- else:
- kwargs['function'] = 'ordinal_num'
- return number_to_word(i, **kwargs)
-
-ordinal_functions = {
- 'en': ordinal_function_en,
- '*': ordinal_function_en
-}
-
-
-def fix_punctuation(text, mark=None, other_marks=None):
- """Ensure the text ends with a punctuation mark, adding one if necessary.
-
- Args:
- text (str): The text to check.
- mark (str, optional): The punctuation mark to append if none is
- present. Defaults to ``'.'``.
- other_marks (list, optional): A list of punctuation marks that are
- considered acceptable endings. Defaults to ``['.', '?', '!']``.
-
- Returns:
- str: The text, possibly with a punctuation mark appended.
- """
- ensure_definition(text, mark, other_marks)
- if other_marks is None:
- other_marks = ['.', '?', '!']
- if not isinstance(other_marks, list):
- other_marks = list(other_marks)
- if mark is None:
- mark = '.'
- text = text.rstrip()
- if mark == '':
- return text
- for end_mark in set([mark] + other_marks):
- if text.endswith(end_mark):
- return text
- return text + mark
-
-
def item_label(num, level=None, punctuation=True):
"""Return a formatted list item label for a given zero-based index and outline level.
@@ -2919,1902 +2903,131 @@ def roman(num, case=None):
return result
-def words():
- return word_collection[this_thread.language]
-
-
-class LazyWord:
-
- def __init__(self, *args, **kwargs):
- if len(kwargs) > 0:
- self.original = args[0] % kwargs
- else:
- self.original = args[0]
-
- def __mod__(self, other):
- return word(self.original) % other
-
- def __str__(self):
- return word(self.original)
-
-
-class LazyArray:
-
- def __init__(self, array):
- self.original = array
-
- def compute(self):
- return [word(item) for item in self.original]
-
- def copy(self):
- return self.compute().copy()
-
- def pop(self, *pargs):
- return str(self.original.pop(*pargs))
-
- def __add__(self, other):
- return self.compute() + other
-
- def index(self, *pargs, **kwargs):
- return self.compute().index(*pargs, **kwargs)
-
- def clear(self):
- self.original = []
-
- def append(self, other):
- self.original.append(other)
-
- def remove(self, other):
- self.original.remove(other)
-
- def extend(self, other):
- self.original.extend(other)
-
- def __contains__(self, item):
- return self.compute().__contains__(item)
-
- def __iter__(self):
- return self.compute().__iter__()
-
- def __len__(self):
- return self.compute().__len__()
-
- def __delitem__(self, index):
- self.original.__delitem__(index)
-
- def __reversed__(self):
- return self.compute().__reversed__()
-
- def __setitem__(self, index, the_value):
- return self.original.__setitem__(index, the_value)
-
- def __getitem__(self, index):
- return self.compute()[index]
-
- def __str__(self):
- return str(self.compute())
-
- def __repr__(self):
- return repr(self.compute())
-
- def __eq__(self, other):
- return self.original == other
-
-
-def word(the_word, **kwargs):
- """Return the word translated into the current language.
-
- If no translation is found for the current language, the input is
- returned unchanged. Used throughout docassemble to support
- multilingual interviews.
-
- Args:
- the_word (str): The word or phrase to translate.
- **kwargs: Optional keyword arguments. Pass ``language`` to
- look up a translation for a specific language, or
- ``capitalize=True`` to capitalize the result.
-
- Returns:
- str: The translated (or original) word.
- """
- # Currently, no kwargs are used, but in the future, this function could be
- # expanded to use kwargs. For example, for languages with gendered words,
- # the gender could be passed as a keyword argument.
- if the_word is True:
- the_word = 'yes'
- elif the_word is False:
- the_word = 'no'
- elif the_word is None:
- the_word = "I don't know"
- if isinstance(the_word, LazyWord):
- the_word = the_word.original
- try:
- the_word = word_collection[kwargs.get('language', this_thread.language)][the_word]
- except:
- the_word = str(the_word)
- if kwargs.get('capitalize', False):
- return capitalize(the_word)
- return the_word
-
-
-def update_language_function(lang, term, func):
- if term not in language_functions:
- language_functions[term] = {}
- language_functions[term][lang] = func
-
-
-def update_nice_numbers(lang, defs):
- if lang not in nice_numbers:
- nice_numbers[lang] = {}
- for number, the_word in defs.items():
- nice_numbers[lang][str(number)] = the_word
-
-
-def update_ordinal_numbers(lang, defs):
- if lang not in ordinal_numbers:
- ordinal_numbers[lang] = {}
- for number, the_word in defs.items():
- ordinal_numbers[lang][str(number)] = the_word
-
-
-def update_ordinal_function(lang, func):
- ordinal_functions[lang] = func
-
-
-def update_word_collection(lang, defs):
- if lang not in word_collection:
- word_collection[lang] = {}
- for the_word, translation in defs.items():
- if translation is not None:
- word_collection[lang][the_word] = translation
-
-# def set_da_config(config):
-# global daconfig
-# daconfig = config
-
-
-def get_config(key, none_value=None):
- """Return a value from the docassemble configuration file.
-
- Args:
- key (str): The configuration directive to look up.
- none_value (optional): The value to return if the key is not found in
- the configuration. Defaults to None.
-
- Returns:
- The configuration value associated with the key, or ``none_value``
- if the key is not present.
- """
- return server.daconfig.get(key, none_value)
-
-# def set_default_language(lang):
-# global default_language
-# default_language = lang
-
-# def set_default_dialect(dialect):
-# global default_dialect
-# default_dialect = dialect
-# return
-
-# def set_default_country(country):
-# global default_country
-# default_country = country
-# return
-
-# def set_default_timezone(timezone):
-# global default_timezone
-# default_timezone = timezone
-# return
-
-
-def get_default_timezone():
- """Return the default timezone string for the server.
-
- Returns the server's local timezone unless a default timezone is configured
- in the docassemble configuration.
-
- Returns:
- str: A timezone string such as ``'America/New_York'``.
- """
- return server.default_timezone
-
-# def reset_thread_local():
-# this_thread.open_files = set()
-# this_thread.temporary_resources = set()
-
-# def reset_thread_variables():
-# this_thread.saved_files = {}
-# this_thread.message_log = []
-
-
-def reset_local_variables():
- # logmessage("reset_local_variables")
- this_thread.language = server.default_language
- this_thread.dialect = server.default_dialect
- this_thread.voice = server.default_voice
- this_thread.country = server.default_country
- this_thread.locale = server.default_locale
- this_thread.session_id = None
- this_thread.interview = None
- this_thread.interview_status = None
- this_thread.evaluation_context = None
- this_thread.gathering_mode = {}
- this_thread.global_vars = GenericObject()
- this_thread.current_variable = []
- # this_thread.template_vars = []
- this_thread.open_files = set()
- this_thread.saved_files = {}
- this_thread.message_log = []
- this_thread.misc = {}
- this_thread.probing = False
- this_thread.current_info = {}
- this_thread.current_package = None
- this_thread.current_question = None
- this_thread.current_section = None
- this_thread.internal = {}
- this_thread.markdown = markdown.Markdown(extensions=['smarty', 'markdown.extensions.sane_lists', 'markdown.extensions.tables', 'markdown.extensions.attr_list', 'markdown.extensions.md_in_html', 'footnotes'], output_format='html5')
- this_thread.prevent_going_back = False
-
-
-def prevent_going_back():
- """Disable the back button so the user cannot revisit previous questions.
-
- Once called, the user will not be able to go back and change any answers
- entered before this point in the interview.
- """
- this_thread.prevent_going_back = True
-
-
-def set_language(lang, dialect=None, voice=None):
- """Set the language used for linguistic functions and the web application.
-
- Does not change the Python locale; call ``update_locale()`` for that.
- Should be called in an ``initial`` code block so it takes effect on every
- page load.
-
- Args:
- lang (str): A lowercase ISO-639-1 or ISO-639-3 language code
- (e.g., ``'en'``, ``'es'``, ``'fr'``).
- dialect (str, optional): A dialect code for the text-to-speech engine.
- Defaults to None.
- voice (str, optional): A voice name for the text-to-speech engine.
- Defaults to None.
- """
- try:
- if dialect:
- this_thread.dialect = dialect
- elif lang != this_thread.language:
- this_thread.dialect = None
- except:
- pass
- try:
- if voice:
- this_thread.voice = voice
- elif lang != this_thread.language:
- this_thread.voice = None
- except:
- pass
- this_thread.language = lang
-
-
-def get_language():
- """Return the current language code.
-
- Returns:
- str: The current language code (e.g., ``'en'``, ``'es'``).
- """
- return this_thread.language
-
-
-def set_country(country):
- """Set the current country used for phone number formatting and other locale features.
-
- Args:
- country (str): A two-letter uppercase ISO 3166-1 alpha-2 country code
- (e.g., ``'US'``, ``'GB'``, ``'DE'``).
- """
- this_thread.country = country
-
-
-def get_country():
- """Return the current country code.
-
- Returns:
- str: A two-letter uppercase ISO 3166-1 alpha-2 country code
- (e.g., ``'US'``). Defaults to ``'US'`` unless configured otherwise.
- """
- return this_thread.country
-
-
-def get_dialect():
- """Return the current dialect.
-
- Returns:
- str: The dialect code set by the ``dialect`` keyword argument to
- :func:`set_language`, or ``None`` if no dialect has been set.
- """
- return this_thread.dialect
-
-
-def get_voice():
- """Return the current voice.
-
- Returns:
- str: The voice name set by the ``voice`` keyword argument to
- :func:`set_language`, or ``None`` if no voice has been set.
- """
- return this_thread.voice
-
-
-def set_locale(*pargs, **kwargs):
- """Set the current locale string and/or locale convention overrides.
-
- Calling ``set_locale('FR.utf8')`` stores the locale string so that
- :func:`get_locale` returns it. The actual Python locale does not change
- until :func:`update_locale` is called. Keyword arguments such as
- ``currency_symbol`` override individual locale conventions used by
- functions like :func:`currency` and :func:`currency_symbol`.
-
- Args:
- *pargs: An optional locale string (e.g. ``'FR.utf8'``).
- **kwargs: Locale convention overrides (e.g. ``currency_symbol='€'``).
- """
- if len(pargs) == 1:
- this_thread.locale = pargs[0]
- if len(kwargs):
- this_thread.misc['locale_overrides'] = kwargs
-
-
-def get_locale(*pargs):
- """Return the current locale setting or a specific locale convention.
-
- With no arguments, returns the locale string previously set with
- :func:`set_locale`. With one argument, returns the value of the named
- locale convention (e.g. ``'currency_symbol'``), taking into account any
- overrides set with :func:`set_locale`.
-
- Args:
- *pargs: An optional locale convention name (e.g.
- ``'currency_symbol'``).
-
- Returns:
- str or None: The locale string when called with no arguments, or the
- value of the requested locale convention (``None`` if not found).
- """
- if len(pargs) == 1:
- if 'locale_overrides' in this_thread.misc and pargs[0] in this_thread.misc['locale_overrides']:
- return this_thread.misc['locale_overrides'][pargs[0]]
- return locale.localeconv().get(pargs[0], None)
- return this_thread.locale
-
-
-def get_currency_symbol():
- """Returns the current setting for the currency symbol if there is
- one, and otherwise returns the default currency symbol.
-
- """
- if 'locale_overrides' in this_thread.misc and 'currency_symbol' in this_thread.misc['locale_overrides']:
- return this_thread.misc['locale_overrides']['currency_symbol']
- return currency_symbol()
-
-
-def update_locale():
- """Update the Python locale based on the current language and locale settings.
-
- Applies the locale string previously set with :func:`set_locale` (combined
- with the current language from :func:`get_language` when necessary) so that
- Python's ``locale`` module reflects the desired locale. This is required
- for functions like :func:`currency` and :func:`currency_symbol` to produce
- locale-appropriate formatting.
- """
- if '_' in this_thread.locale:
- the_locale = str(this_thread.locale)
- else:
- the_locale = str(this_thread.language) + '_' + str(this_thread.locale)
- try:
- locale.setlocale(locale.LC_ALL, the_locale)
- except BaseException as err:
- logmessage("update_locale error: unable to set the locale to " + the_locale)
- logmessage(err.__class__.__name__ + ": " + str(err))
- locale.setlocale(locale.LC_ALL, 'en_US.utf8')
-
-
-def comma_list_en(*pargs, **kwargs):
- """Returns the arguments separated by commas. If the first argument is a list,
- that list is used. Otherwise, the arguments are treated as individual items.
- See also comma_and_list()."""
- ensure_definition(*pargs, **kwargs)
- comma_string = kwargs.get('comma_string', ', ')
- the_list = []
- for parg in pargs:
- if isinstance(parg, str):
- the_list.append(parg)
- elif (hasattr(parg, 'instanceName') and hasattr(parg, 'elements')) or isinstance(parg, Iterable):
- for sub_parg in parg:
- the_list.append(str(sub_parg))
- else:
- the_list.append(str(parg))
- return comma_string.join(the_list)
-
-
-def comma_and_list_es(*pargs, **kwargs):
- if 'and_string' not in kwargs:
- kwargs['and_string'] = 'y'
- return comma_and_list_en(*pargs, **kwargs)
-
-
-def comma_and_list_de(*pargs, **kwargs):
- if 'and_string' not in kwargs:
- kwargs['and_string'] = 'und'
- if 'oxford' not in kwargs:
- kwargs['oxford'] = False
- return comma_and_list_en(*pargs, **kwargs)
-
-
-def comma_and_list_en(*pargs, **kwargs):
- """Returns an English-language listing of the arguments. If the first argument is a list,
- that list is used. Otherwise, the arguments are treated as individual items in the list.
- Use the optional argument oxford=False if you do not want a comma before the "and."
- See also comma_list()."""
- ensure_definition(*pargs, **kwargs)
- and_string = kwargs.get('and_string', word('and'))
- comma_string = kwargs.get('comma_string', ', ')
- if 'oxford' in kwargs and kwargs['oxford'] is False:
- extracomma = ""
- else:
- extracomma = comma_string.strip()
- before_and = kwargs.get('before_and', ' ')
- after_and = kwargs.get('after_and', ' ')
- the_list = []
- for parg in pargs:
- if isinstance(parg, str):
- the_list.append(parg)
- elif (hasattr(parg, 'instanceName') and hasattr(parg, 'elements')) or isinstance(parg, Iterable):
- for sub_parg in parg:
- the_list.append(str(sub_parg))
- else:
- the_list.append(str(parg))
- if len(the_list) == 0:
- return str('')
- if len(the_list) == 1:
- return the_list[0]
- if len(the_list) == 2:
- return the_list[0] + before_and + and_string + after_and + the_list[1]
- return comma_string.join(the_list[:-1]) + extracomma + before_and + and_string + after_and + the_list[-1]
-
-
-def manual_line_breaks(text):
- """Replaces newlines with manual line breaks."""
- if this_thread.evaluation_context == 'docx':
- return re.sub(r' *\r?\n *', '', str(text))
- return re.sub(r' *\r?\n *', ' [BR] ', str(text))
-
-
-def add_separators_en(*pargs, **kwargs):
- """Accepts a list and returns a list, with semicolons after each item,
- except "and" after the penultimate item and a period after the
- last.
-
- """
- ensure_definition(*pargs, **kwargs)
- separator = kwargs.get('separator', ';')
- last_separator = kwargs.get('last_separator', '; ' + word("and"))
- end_mark = kwargs.get('end_mark', '.')
- the_list = []
- for parg in pargs:
- if isinstance(parg, str):
- the_list.append(parg.rstrip())
- elif (hasattr(parg, 'instanceName') and hasattr(parg, 'elements')) or isinstance(parg, Iterable):
- for sub_parg in parg:
- the_list.append(str(sub_parg).rstrip())
- else:
- the_list.append(str(parg).rstrip())
- if len(the_list) == 0:
- return the_list
- if len(the_list) == 1:
- return [fix_punctuation(the_list[0], mark=end_mark)]
- for indexno in range(len(the_list) - 2): # for 4: 0, 1; for 3: 0; for 2: []
- the_list[indexno] = the_list[indexno].rstrip(',')
- the_list[indexno] = fix_punctuation(the_list[indexno], mark=separator)
- if not the_list[-2].endswith(last_separator):
- the_list[-2] = the_list[-2].rstrip(last_separator[0])
- the_list[-2] += last_separator
- the_list[-1] = fix_punctuation(the_list[-1], mark=end_mark)
- return the_list
-
-
-def need(*pargs):
- """Ensure that the given variables are defined, asking questions if necessary.
-
- Evaluating each argument causes docassemble to seek its definition
- through the normal interview logic. The function always returns
- ``True``. Using ``need()`` is purely for readability; writing
- ``need(x, y)`` is equivalent to writing ``x; y`` in a code block.
-
- Args:
- *pargs: Variables whose definitions should be ensured.
-
- Returns:
- bool: Always ``True``.
- """
- ensure_definition(*pargs)
- for argument in pargs:
- argument # pylint: disable=pointless-statement
- return True
-
-
-def pickleable_objects(input_dict):
- output_dict = {}
- for key in input_dict:
- if isinstance(input_dict[key], (types.ModuleType, types.FunctionType, TypeType, types.BuiltinFunctionType, types.BuiltinMethodType, types.MethodType, FileType)):
- continue
- if key == "__builtins__":
- continue
- output_dict[key] = input_dict[key]
- return output_dict
-
-
-def ordinal_number_default(the_number, **kwargs):
- """Returns the "first," "second," "third," etc. for a given number.
- ordinal_number(1) returns "first." For a function that can be used
- on index numbers that start with zero, see ordinal()."""
- num = str(the_number)
- if kwargs.get('use_word', True):
- if this_thread.language in ordinal_numbers and num in ordinal_numbers[this_thread.language]:
- return ordinal_numbers[this_thread.language][num]
- if '*' in ordinal_numbers and num in ordinal_numbers['*']:
- return ordinal_numbers['*'][num]
- if this_thread.language in ordinal_functions:
- language_to_use = this_thread.language
- elif '*' in ordinal_functions:
- language_to_use = '*'
- else:
- language_to_use = 'en'
- return ordinal_functions[language_to_use](the_number, **kwargs)
-
-
-def salutation_default(indiv, **kwargs):
- """Returns Mr., Ms., etc. for an individual."""
- with_name = kwargs.get('with_name', False)
- with_name_and_punctuation = kwargs.get('with_name_and_punctuation', False)
- ensure_definition(indiv, with_name, with_name_and_punctuation)
- used_gender = False
- if hasattr(indiv, 'salutation_to_use') and indiv.salutation_to_use is not None:
- salut = indiv.salutation_to_use
- elif hasattr(indiv, 'is_doctor') and indiv.is_doctor:
- salut = 'Dr.'
- elif hasattr(indiv, 'is_judge') and indiv.is_judge:
- salut = 'Judge'
- elif hasattr(indiv, 'name') and hasattr(indiv.name, 'suffix') and indiv.name.suffix in ('MD', 'PhD'):
- salut = 'Dr.'
- elif hasattr(indiv, 'name') and hasattr(indiv.name, 'suffix') and indiv.name.suffix == 'J':
- salut = 'Judge'
- elif indiv.gender == 'female':
- used_gender = True
- salut = 'Ms.'
- else:
- used_gender = True
- salut = 'Mr.'
- if with_name_and_punctuation or with_name:
- if used_gender and indiv.gender not in ('male', 'female'):
- salut_and_name = indiv.name.full()
- else:
- salut_and_name = salut + ' ' + indiv.name.last
- if with_name_and_punctuation:
- if hasattr(indiv, 'is_friendly') and indiv.is_friendly:
- punct = ','
- else:
- punct = ':'
- return salut_and_name + punct
- if with_name:
- return salut_and_name
- return salut
-
-
-def string_to_number(number):
- try:
- float_number = float(number)
- int_number = int(number)
- if float_number == int_number:
- return int_number
- return float_number
- except:
- return number
-
-
-def number_to_word(number, **kwargs):
- language = kwargs.get('language', None)
- capitalize_arg = kwargs.get('capitalize', False)
- function = kwargs.get('function', None)
- raise_on_error = kwargs.get('raise_on_error', False)
- if function not in ('ordinal', 'ordinal_num'):
- function = 'cardinal'
- if language is None:
- language = get_language()
- for lang, loc in (('en', 'en_GB'), ('en', 'en_IN'), ('es', 'es_CO'), ('es', 'es_VE'), ('fr', 'fr_CH'), ('fr', 'fr_BE'), ('fr', 'fr_DZ'), ('pt', 'pt_BR')):
- if language == lang and this_thread.locale.startswith(loc):
- language = loc
- break
- number = string_to_number(number)
- if raise_on_error:
- the_word = num2words.num2words(number, lang=language, to=function)
- else:
- try:
- the_word = num2words.num2words(number, lang=language, to=function)
- except NotImplementedError:
- the_word = str(number)
- if capitalize_arg:
- return capitalize_function(the_word)
- return the_word
-
-
-def ordinal_default(the_number, **kwargs):
- """Returns the "first," "second," "third," etc. for a given number, which is expected to
- be an index starting with zero. ordinal(0) returns "first." For a more literal ordinal
- number function, see ordinal_number()."""
- result = ordinal_number(int(float(the_number)) + 1, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(result)
- return result
-
-
-def nice_number_default(the_number, **kwargs):
- """Returns the number as a word in the current language."""
- capitalize_arg = kwargs.get('capitalize', False)
- language = kwargs.get('language', None)
- use_word = kwargs.get('use_word', None)
- ensure_definition(the_number, capitalize_arg, language)
- if language is None:
- language = this_thread.language
- if language in nice_numbers:
- language_to_use = language
- elif '*' in nice_numbers:
- language_to_use = '*'
- else:
- language_to_use = 'en'
- if isinstance(the_number, float):
- the_number = float(decimal.Context(prec=8).create_decimal_from_float(the_number))
- if int(float(the_number)) == float(the_number):
- the_number = int(float(the_number))
- is_integer = True
- else:
- is_integer = False
- if language_to_use in nice_numbers and str(the_number) in nice_numbers[language_to_use]:
- the_word = nice_numbers[language_to_use][str(the_number)]
- if capitalize_arg:
- return capitalize_function(the_word)
- return the_word
- if use_word or (is_integer and 0 <= the_number < 11 and use_word is not False):
- try:
- return number_to_word(the_number, **kwargs)
- except:
- pass
- if isinstance(the_number, int):
- return str(locale.format_string("%d", the_number, grouping=True))
- return str(locale.format_string("%.2f", float(the_number), grouping=True)).rstrip('0')
-
-
-def quantity_noun_default(the_number, noun, **kwargs):
- as_integer = kwargs.get('as_integer', True)
- capitalize_arg = kwargs.get('capitalize', False)
- language = kwargs.get('language', None)
- ensure_definition(the_number, noun, as_integer, capitalize_arg, language)
- if as_integer:
- the_number = int(round(the_number))
- result = nice_number(the_number, language=language) + " " + noun_plural(noun, the_number, language=language)
- if capitalize_arg:
- return capitalize_function(result)
- return result
-
-
-def capitalize_default(a, **kwargs): # pylint: disable=unused-argument
- ensure_definition(a)
- if not isinstance(a, str):
- a = str(a)
- if a and len(a) > 1:
- return a[0].upper() + a[1:]
- return a
-
-
-def currency_symbol_default(**kwargs): # pylint: disable=unused-argument
- """Returns the currency symbol for the current locale."""
- return str(locale.localeconv()['currency_symbol'])
-
-
-def currency_default(the_value, **kwargs):
- """Returns the value as a currency, according to the conventions of
- the current locale. Use the optional keyword argument
- decimals=False if you do not want to see decimal places in the
- number, and the optional currency_symbol for a different symbol
- than the default.
-
- """
- decimals = kwargs.get('decimals', True)
- symbol = kwargs.get('symbol', None)
- symbol_precedes = kwargs.get('symbol_precedes', None)
- ensure_definition(the_value, decimals, symbol)
- obj_type = type(the_value).__name__
- if obj_type in ['FinancialList', 'PeriodicFinancialList']:
- the_value = the_value.total()
- elif obj_type in ['Value', 'PeriodicValue']:
- if the_value.exists:
- the_value = the_value.amount()
- else:
- the_value = 0
- elif obj_type == 'DACatchAll':
- the_value = float(the_value)
- try:
- float(the_value)
- except:
- return ''
- the_float_value = float(the_value)
- the_symbol = None
- if symbol is not None:
- the_symbol = symbol
- elif 'locale_overrides' in this_thread.misc and 'currency_symbol' in this_thread.misc['locale_overrides']:
- the_symbol = this_thread.misc['locale_overrides']['currency_symbol']
- elif language_functions['currency_symbol']['*'] is not currency_symbol_default:
- the_symbol = currency_symbol()
- the_symbol_precedes = None
- if symbol_precedes is not None:
- the_symbol_precedes = symbol_precedes
- elif 'locale_overrides' in this_thread.misc and the_float_value < 0 and 'n_cs_precedes' in this_thread.misc['locale_overrides']:
- the_symbol_precedes = bool(this_thread.misc['locale_overrides']['n_cs_precedes'])
- elif 'locale_overrides' in this_thread.misc and 'p_cs_precedes' in this_thread.misc['locale_overrides']:
- the_symbol_precedes = bool(this_thread.misc['locale_overrides']['p_cs_precedes'])
- if the_symbol is None and the_symbol_precedes is None and decimals:
- return str(locale.currency(the_float_value, symbol=True, grouping=True))
- if the_symbol is None:
- the_symbol = currency_symbol()
- if the_symbol_precedes is None:
- if the_float_value < 0:
- the_symbol_precedes = bool(get_locale('n_cs_precedes'))
- else:
- the_symbol_precedes = bool(get_locale('p_cs_precedes'))
- output = ''
- if the_symbol_precedes:
- output += the_symbol
- if the_float_value < 0:
- if get_locale('n_sep_by_space'):
- output += ' '
- elif get_locale('p_sep_by_space'):
- output += ' '
- if decimals:
- output += locale.format_string('%.' + str(server.daconfig.get('currency decimal places', locale.localeconv()['frac_digits'])) + 'f', the_float_value, grouping=True, monetary=True)
- else:
- output += locale.format_string("%d", int(the_float_value), grouping=True, monetary=True)
- if not the_symbol_precedes:
- if the_float_value < 0:
- if get_locale('n_sep_by_space'):
- output += ' '
- elif get_locale('p_sep_by_space'):
- output += ' '
- output += the_symbol
- return output
-
-
-def prefix_constructor(prefix):
-
- def func(the_word, **kwargs):
- ensure_definition(the_word, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(str(prefix)) + str(the_word)
- return str(prefix) + str(the_word)
- return func
-
-
-def double_prefix_constructor_reverse(prefix_one, prefix_two):
-
- def func(word_one, word_two, **kwargs):
- ensure_definition(word_one, word_two, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(str(prefix_one)) + str(word_two) + str(prefix_two) + str(word_one)
- return str(prefix_one) + str(word_two) + str(prefix_two) + str(word_one)
- return func
-
-
-def prefix_constructor_two_arguments(prefix, **kwargs): # pylint: disable=unused-argument
-
- def func(word_one, word_two, **kwargs):
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(str(prefix)) + str(word_one) + ' ' + str(word_two)
- return str(prefix) + str(word_one) + ' ' + str(word_two)
- return func
-
-
-def middle_constructor(middle, **kwargs): # pylint: disable=unused-argument
-
- def func(a, b, **kwargs):
- ensure_definition(a, b, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(str(a)) + str(middle) + str(b)
- return str(a) + str(middle) + str(b)
- return func
-
-
-def possessify_en(a, b, **kwargs):
- ensure_definition(a, b, **kwargs)
- if this_thread.evaluation_context == 'docx':
- apostrophe = "’"
- else:
- apostrophe = "'"
- if 'plural' in kwargs and kwargs['plural']:
- middle = apostrophe + " "
- else:
- middle = apostrophe + "s "
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(str(a)) + str(middle) + str(b)
- return str(a) + str(middle) + str(b)
-
-
-def a_preposition_b_default(a, b, **kwargs):
- ensure_definition(a, b, **kwargs)
- if hasattr(a, 'preposition'):
- preposition = word(a.preposition)
- else:
- preposition = word('in the')
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(str(a)) + str(' ' + preposition + ' ') + str(b)
- return str(a) + str(' ' + preposition + ' ') + str(b)
-
-
-def verb_present_en(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(str(arg))
- if len(new_args) < 2:
- new_args.append('3sg')
- output = pattern_en.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_past_en(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(arg)
- if len(new_args) < 2:
- new_args.append('3sgp')
- output = pattern_en.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def number_or_length(target):
- if isinstance(target, (int, float)):
- return target
- if isinstance(target, (list, dict, set, tuple)) or (hasattr(target, 'elements') and isinstance(target.elements, (list, dict, set))):
- return len(target)
- if target:
- return 2
- return 1
-
-
-def noun_plural_en(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if kwargs.get('noun_is_singular', False):
- noun = pargs[0]
- else:
- noun = noun_singular_en(pargs[0])
- if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
- return str(noun)
- output = pattern_en.pluralize(str(noun))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_singular_en(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
- return pargs[0]
- output = pattern_en.singularize(str(pargs[0]))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def indefinite_article_en(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- output = pattern_en.article(str(pargs[0]).lower()) + " " + str(pargs[0])
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_present_es(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(str(arg))
- if len(new_args) < 2:
- new_args.append('3sg')
- if new_args[1] == 'pl':
- new_args[1] = '3pl'
- output = pattern_es.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_past_es(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(arg)
- if len(new_args) < 2:
- new_args.append('3sgp')
- if new_args[1] == 'ppl':
- new_args[1] = '3ppl'
- output = pattern_es.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_plural_es(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if kwargs.get('noun_is_singular', False):
- noun = pargs[0]
- else:
- noun = noun_singular_es(pargs[0])
- if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
- return str(noun)
- output = pattern_es.pluralize(str(noun))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_singular_es(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
- return pargs[0]
- output = pattern_es.singularize(str(pargs[0]))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def indefinite_article_es(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- output = pattern_es.article(str(pargs[0]).lower()) + " " + str(pargs[0])
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_present_de(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(str(arg))
- if len(new_args) < 2:
- new_args.append('3sg')
- if new_args[1] == 'pl':
- new_args[1] = '3pl'
- output = pattern_de.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_past_de(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(arg)
- if len(new_args) < 2:
- new_args.append('3sgp')
- if new_args[1] == 'ppl':
- new_args[1] = '3ppl'
- output = pattern_de.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_plural_de(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if kwargs.get('noun_is_singular', False):
- noun = pargs[0]
- else:
- noun = noun_singular_de(pargs[0])
- if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
- return str(noun)
- output = pattern_de.pluralize(str(noun))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_singular_de(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
- return pargs[0]
- output = pattern_de.singularize(str(pargs[0]))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def indefinite_article_de(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- output = pattern_de.article(str(pargs[0]).lower()) + " " + str(pargs[0])
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_present_fr(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(str(arg))
- if len(new_args) < 2:
- new_args.append('3sg')
- if new_args[1] == 'pl':
- new_args[1] = '3pl'
- output = pattern_fr.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_past_fr(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(arg)
- if len(new_args) < 2:
- new_args.append('3sgp')
- if new_args[1] == 'ppl':
- new_args[1] = '3ppl'
- output = pattern_fr.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_plural_fr(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if kwargs.get('noun_is_singular', False):
- noun = pargs[0]
- else:
- noun = noun_singular_fr(pargs[0])
- if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
- return str(noun)
- output = pattern_fr.pluralize(str(noun))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_singular_fr(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
- return pargs[0]
- output = pattern_fr.singularize(str(pargs[0]))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def indefinite_article_fr(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- output = pattern_fr.article(str(pargs[0]).lower()) + " " + str(pargs[0])
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_present_it(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(str(arg))
- if len(new_args) < 2:
- new_args.append('3sg')
- if new_args[1] == 'pl':
- new_args[1] = '3pl'
- output = pattern_it.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_past_it(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(arg)
- if len(new_args) < 2:
- new_args.append('3sgp')
- if new_args[1] == 'ppl':
- new_args[1] = '3ppl'
- output = pattern_it.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_plural_it(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if kwargs.get('noun_is_singular', False):
- noun = pargs[0]
- else:
- noun = noun_singular_it(pargs[0])
- if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
- return str(noun)
- output = pattern_it.pluralize(str(noun))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_singular_it(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
- return pargs[0]
- output = pattern_it.singularize(str(pargs[0]))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def indefinite_article_it(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- output = pattern_it.article(str(pargs[0]).lower()) + " " + str(pargs[0])
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_present_nl(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(str(arg))
- if len(new_args) < 2:
- new_args.append('3sg')
- if new_args[1] == 'pl':
- new_args[1] = '3pl'
- output = pattern_nl.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def verb_past_nl(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- new_args = []
- for arg in pargs:
- new_args.append(arg)
- if len(new_args) < 2:
- new_args.append('3sgp')
- if new_args[1] == 'ppl':
- new_args[1] = '3ppl'
- output = pattern_nl.conjugate(*new_args, **kwargs)
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_plural_nl(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if kwargs.get('noun_is_singular', False):
- noun = pargs[0]
- else:
- noun = noun_singular_nl(pargs[0])
- if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
- return str(noun)
- output = pattern_nl.pluralize(str(noun))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def noun_singular_nl(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
- return pargs[0]
- output = pattern_nl.singularize(str(pargs[0]))
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def indefinite_article_nl(*pargs, **kwargs):
- ensure_definition(*pargs, **kwargs)
- output = pattern_nl.article(str(pargs[0]).lower()) + " " + str(pargs[0])
- if 'capitalize' in kwargs and kwargs['capitalize']:
- return capitalize(output)
- return output
-
-
-def titlecasestr(text):
- return titlecase.titlecase(str(text))
-
-language_functions = {
- 'in_the': {
- 'en': prefix_constructor('in the ')
- },
- 'a_preposition_b': {
- 'en': a_preposition_b_default
- },
- 'a_in_the_b': {
- 'en': middle_constructor(' in the ')
- },
- 'i_subjective': {
- 'en': lambda *pargs, **kwargs: word('I', **kwargs)
- },
- 'he_subjective': {
- 'en': lambda *pargs, **kwargs: word('he', **kwargs)
- },
- 'she_subjective': {
- 'en': lambda *pargs, **kwargs: word('she', **kwargs)
- },
- 'genderless_subjective': {
- 'en': lambda *pargs, **kwargs: word('they', **kwargs)
- },
- 'myself': {
- 'en': lambda *pargs, **kwargs: word('myself', **kwargs)
- },
- 'itself': {
- 'en': lambda *pargs, **kwargs: word('itself', **kwargs)
- },
- 'herself': {
- 'en': lambda *pargs, **kwargs: word('herself', **kwargs)
- },
- 'himself': {
- 'en': lambda *pargs, **kwargs: word('himself', **kwargs)
- },
- 'themselves': {
- 'en': lambda *pargs, **kwargs: word('themselves', **kwargs)
- },
- 'genderless_self': {
- 'en': lambda *pargs, **kwargs: word('themself', **kwargs)
- },
- 'yourself': {
- 'en': lambda *pargs, **kwargs: word('yourself', **kwargs)
- },
- 'yourselves': {
- 'en': lambda *pargs, **kwargs: word('yourselves', **kwargs)
- },
- 'ourselves': {
- 'en': lambda *pargs, **kwargs: word('ourselves', **kwargs)
- },
- 'you_subjective': {
- 'en': lambda *pargs, **kwargs: word('you', **kwargs)
- },
- 'you_subjective_plural': {
- 'en': lambda *pargs, **kwargs: word('you', **kwargs)
- },
- 'we_subjective': {
- 'en': lambda *pargs, **kwargs: word('we', **kwargs)
- },
- 'they_subjective': {
- 'en': lambda *pargs, **kwargs: word('they', **kwargs)
- },
- 'it_subjective': {
- 'en': lambda *pargs, **kwargs: word('it', **kwargs)
- },
- 'it_objective': {
- 'en': lambda *pargs, **kwargs: word('it', **kwargs)
- },
- 'them_objective': {
- 'en': lambda *pargs, **kwargs: word('them', **kwargs)
- },
- 'genderless_objective': {
- 'en': lambda *pargs, **kwargs: word('them', **kwargs)
- },
- 'me_objective': {
- 'en': lambda *pargs, **kwargs: word('me', **kwargs)
- },
- 'him_objective': {
- 'en': lambda *pargs, **kwargs: word('him', **kwargs)
- },
- 'her_objective': {
- 'en': lambda *pargs, **kwargs: word('her', **kwargs)
- },
- 'you_objective': {
- 'en': lambda *pargs, **kwargs: word('you', **kwargs)
- },
- 'you_objective_plural': {
- 'en': lambda *pargs, **kwargs: word('you', **kwargs)
- },
- 'us_objective': {
- 'en': lambda *pargs, **kwargs: word('us', **kwargs)
- },
- 'are_we': {
- 'en': lambda *pargs, **kwargs: word('are we', **kwargs)
- },
- 'are_you': {
- 'en': lambda *pargs, **kwargs: word('are you', **kwargs)
- },
- 'are_you_plural': {
- 'en': lambda *pargs, **kwargs: word('are you', **kwargs)
- },
- 'am_i': {
- 'en': lambda *pargs, **kwargs: word('am I', **kwargs)
- },
- 'her': {
- 'en': prefix_constructor('her ')
- },
- 'his': {
- 'en': prefix_constructor('his ')
- },
- 'are_word': {
- 'en': prefix_constructor('are ')
- },
- 'is_word': {
- 'en': prefix_constructor('is ')
- },
- 'their': {
- 'en': prefix_constructor('their ')
- },
- 'my_possessive': {
- 'en': prefix_constructor('my ')
- },
- 'our_possessive': {
- 'en': prefix_constructor('our ')
- },
- 'of_the': {
- 'en': prefix_constructor('of the ')
- },
- 'your': {
- 'en': prefix_constructor('your ')
- },
- 'your_plural': {
- 'en': prefix_constructor('your ')
- },
- 'some': {
- 'en': prefix_constructor('some ')
- },
- 'its': {
- 'en': prefix_constructor('its ')
- },
- 'the': {
- 'en': prefix_constructor('the ')
- },
- 'these': {
- 'en': prefix_constructor('these ')
- },
- 'this': {
- 'en': prefix_constructor('this ')
- },
- 'does_a_b': {
- 'en': prefix_constructor_two_arguments('does ')
- },
- 'do_a_b': {
- 'en': prefix_constructor_two_arguments('do ')
- },
- 'did_a_b': {
- 'en': prefix_constructor_two_arguments('did ')
- },
- 'did_a_b_plural': {
- 'en': prefix_constructor_two_arguments('did ')
- },
- 'do_i': {
- 'en': prefix_constructor('do I ')
- },
- 'do_we': {
- 'en': prefix_constructor('do we ')
- },
- 'do_you': {
- 'en': prefix_constructor('do you ')
- },
- 'do_you_plural': {
- 'en': prefix_constructor('do you ')
- },
- 'did_i': {
- 'en': prefix_constructor('did I ')
- },
- 'did_we': {
- 'en': prefix_constructor('did we ')
- },
- 'did_you': {
- 'en': prefix_constructor('did you ')
- },
- 'did_you_plural': {
- 'en': prefix_constructor('did you ')
- },
- 'was_i': {
- 'en': prefix_constructor('was I ')
- },
- 'were_we': {
- 'en': prefix_constructor('were we ')
- },
- 'were_you': {
- 'en': prefix_constructor('were you ')
- },
- 'were_you_plural': {
- 'en': prefix_constructor('were you ')
- },
- 'was_a_b': {
- 'en': prefix_constructor_two_arguments('was ')
- },
- 'were_a_b': {
- 'en': prefix_constructor_two_arguments('were ')
- },
- 'were_a_b_plural': {
- 'en': prefix_constructor_two_arguments('were ')
- },
- 'have_i': {
- 'en': prefix_constructor('have I ')
- },
- 'have_we': {
- 'en': prefix_constructor('have we ')
- },
- 'have_you': {
- 'en': prefix_constructor('have you ')
- },
- 'have_you_plural': {
- 'en': prefix_constructor('have you ')
- },
- 'has_a_b': {
- 'en': prefix_constructor_two_arguments('has ')
- },
- 'have_a_b': {
- 'en': prefix_constructor_two_arguments('have ')
- },
- 'verb_past': {
- 'en': verb_past_en,
- 'es': verb_past_es,
- 'de': verb_past_de,
- 'fr': verb_past_fr,
- 'it': verb_past_it,
- 'nl': verb_past_nl
- },
- 'verb_present': {
- 'en': verb_present_en,
- 'es': verb_present_es,
- 'de': verb_present_de,
- 'fr': verb_present_fr,
- 'it': verb_present_it,
- 'nl': verb_present_nl
- },
- 'noun_plural': {
- 'en': noun_plural_en,
- 'es': noun_plural_es,
- 'de': noun_plural_de,
- 'fr': noun_plural_fr,
- 'it': noun_plural_it,
- 'nl': noun_plural_nl
- },
- 'noun_singular': {
- 'en': noun_singular_en,
- 'es': noun_singular_es,
- 'de': noun_singular_de,
- 'fr': noun_singular_fr,
- 'it': noun_singular_it,
- 'nl': noun_singular_nl
- },
- 'indefinite_article': {
- 'en': indefinite_article_en,
- 'es': indefinite_article_es,
- 'de': indefinite_article_de,
- 'it': indefinite_article_it
- },
- 'currency_symbol': {
- '*': currency_symbol_default
- },
- 'period_list': {
- '*': lambda: [[12, word("Per Month")], [1, word("Per Year")], [52, word("Per Week")], [24, word("Twice Per Month")], [26, word("Every Two Weeks")]]
- },
- 'name_suffix': {
- '*': lambda: ['Jr', 'Sr', 'II', 'III', 'IV', 'V', 'VI']
- },
- 'currency': {
- '*': currency_default
- },
- 'possessify': {
- 'en': possessify_en
- },
- 'possessify_long': {
- 'en': double_prefix_constructor_reverse('the ', ' of the ')
- },
- 'comma_and_list': {
- 'en': comma_and_list_en,
- 'es': comma_and_list_es,
- 'de': comma_and_list_de
- },
- 'comma_list': {
- 'en': comma_list_en
- },
- 'add_separators': {
- 'en': add_separators_en
- },
- 'nice_number': {
- '*': nice_number_default
- },
- 'quantity_noun': {
- '*': quantity_noun_default
- },
- 'ordinal_number': {
- '*': ordinal_number_default
- },
- 'ordinal': {
- '*': ordinal_default
- },
- 'capitalize': {
- '*': capitalize_default
- },
- 'title_case': {
- '*': titlecasestr
- },
- 'salutation': {
- '*': salutation_default
- }
-}
-
-
-def language_function_constructor(term):
-
- def func(*args, **kwargs):
- ensure_definition(*args, **kwargs)
- language = kwargs.get('language', None)
- if language is None:
- language = this_thread.language
- if language in language_functions[term]:
- return language_functions[term][language](*args, **kwargs)
- if '*' in language_functions[term]:
- return language_functions[term]['*'](*args, **kwargs)
- if 'en' in language_functions[term]:
- logmessage("Term " + str(term) + " is not defined for language " + str(language))
- return language_functions[term]['en'](*args, **kwargs)
- raise SystemError("term " + str(term) + " not defined in language_functions for English or *")
- return func
-
-in_the = language_function_constructor('in_the')
-a_preposition_b = language_function_constructor('a_preposition_b')
-a_in_the_b = language_function_constructor('a_in_the_b')
-i_subjective = language_function_constructor('i_subjective')
-he_subjective = language_function_constructor('he_subjective')
-she_subjective = language_function_constructor('she_subjective')
-genderless_subjective = language_function_constructor('genderless_subjective')
-myself = language_function_constructor('myself')
-itself = language_function_constructor('itself')
-herself = language_function_constructor('herself')
-himself = language_function_constructor('himself')
-themselves = language_function_constructor('themselves')
-genderless_self = language_function_constructor('genderless_self')
-yourself = language_function_constructor('yourself')
-yourselves = language_function_constructor('yourselves')
-ourselves = language_function_constructor('ourselves')
-you_subjective = language_function_constructor('you_subjective')
-you_subjective_plural = language_function_constructor('you_subjective_plural')
-we_subjective = language_function_constructor('we_subjective')
-they_subjective = language_function_constructor('they_subjective')
-it_subjective = language_function_constructor('it_subjective')
-it_objective = language_function_constructor('it_objective')
-them_objective = language_function_constructor('them_objective')
-genderless_objective = language_function_constructor('genderless_objective')
-me_objective = language_function_constructor('me_objective')
-him_objective = language_function_constructor('him_objective')
-her_objective = language_function_constructor('her_objective')
-you_objective = language_function_constructor('you_objective')
-you_objective_plural = language_function_constructor('you_objective_plural')
-us_objective = language_function_constructor('us_objective')
-are_we = language_function_constructor('are_we')
-are_you = language_function_constructor('are_you')
-are_you_plural = language_function_constructor('are_you_plural')
-am_i = language_function_constructor('am_i')
-her = language_function_constructor('her')
-his = language_function_constructor('his')
-are_word = language_function_constructor('are_word')
-is_word = language_function_constructor('is_word')
-their = language_function_constructor('their')
-my_possessive = language_function_constructor('my_possessive')
-our_possessive = language_function_constructor('our_possessive')
-of_the = language_function_constructor('of_the')
-your = language_function_constructor('your')
-your_plural = language_function_constructor('your_plural')
-some = language_function_constructor('some')
-its = language_function_constructor('its')
-the = language_function_constructor('the')
-these = language_function_constructor('these')
-this = language_function_constructor('this')
-does_a_b = language_function_constructor('does_a_b')
-do_a_b = language_function_constructor('do_a_b')
-did_a_b = language_function_constructor('did_a_b')
-did_a_b_plural = language_function_constructor('did_a_b_plural')
-do_i = language_function_constructor('do_i')
-do_we = language_function_constructor('do_we')
-do_you = language_function_constructor('do_you')
-do_you_plural = language_function_constructor('do_you_plural')
-did_i = language_function_constructor('did_i')
-did_we = language_function_constructor('did_we')
-did_you = language_function_constructor('did_you')
-did_you_plural = language_function_constructor('did_you_plural')
-was_i = language_function_constructor('was_i')
-were_we = language_function_constructor('were_we')
-were_you = language_function_constructor('were_you')
-were_you_plural = language_function_constructor('were_you_plural')
-was_a_b = language_function_constructor('was_a_b')
-were_a_b = language_function_constructor('were_a_b')
-were_a_b_plural = language_function_constructor('were_a_b_plural')
-have_i = language_function_constructor('have_i')
-have_we = language_function_constructor('have_we')
-have_you = language_function_constructor('have_you')
-have_you_plural = language_function_constructor('have_you_plural')
-has_a_b = language_function_constructor('has_a_b')
-have_a_b = language_function_constructor('have_a_b')
-verb_past = language_function_constructor('verb_past')
-verb_present = language_function_constructor('verb_present')
-noun_plural = language_function_constructor('noun_plural')
-noun_singular = language_function_constructor('noun_singular')
-indefinite_article = language_function_constructor('indefinite_article')
-period_list = language_function_constructor('period_list')
-name_suffix = language_function_constructor('name_suffix')
-currency = language_function_constructor('currency')
-currency_symbol = language_function_constructor('currency_symbol')
-possessify = language_function_constructor('possessify')
-possessify_long = language_function_constructor('possessify_long')
-comma_list = language_function_constructor('comma_list')
-comma_and_list = language_function_constructor('comma_and_list')
-add_separators = language_function_constructor('add_separators')
-nice_number = language_function_constructor('nice_number')
-quantity_noun = language_function_constructor('quantity_noun')
-capitalize = language_function_constructor('capitalize')
-capitalize_function = capitalize
-title_case = language_function_constructor('title_case')
-ordinal_number = language_function_constructor('ordinal_number')
-ordinal = language_function_constructor('ordinal')
-salutation = language_function_constructor('salutation')
-
-if verb_past.__doc__ is None:
- verb_past.__doc__ = """Return the past tense of a verb.
-
- Args:
- verb (str): The verb to conjugate.
- **kwargs: Optional conjugation parameters passed to the underlying
- language function (e.g. ``'3gp'`` for third-person past tense).
-
- Returns:
- str: The past-tense form of the verb (e.g. ``verb_past('help')``
- returns ``'helped'``).
- """
-if verb_present.__doc__ is None:
- verb_present.__doc__ = """Return the present tense of a verb.
-
- Args:
- verb (str): The verb to conjugate (may be in any tense).
- **kwargs: Optional conjugation parameters passed to the underlying
- language function (e.g. ``'3sg'`` for third-person singular).
-
- Returns:
- str: The present-tense form of the verb (e.g.
- ``verb_present('helped', '3sg')`` returns ``'helps'``).
- """
-if noun_plural.__doc__ is None:
- noun_plural.__doc__ = """Return the plural form of a noun.
-
- Args:
- noun (str): The noun to pluralize.
- *pargs: An optional quantity (number, list, dict, or set). When the
- quantity is exactly ``1`` the singular form is returned instead.
- **kwargs: Pass ``noun_is_singular=True`` to skip singularization
- before pluralizing.
-
- Returns:
- str: The plural form of the noun, or the singular form if the
- optional quantity equals ``1``.
- """
-if noun_singular.__doc__ is None:
- noun_singular.__doc__ = """Return the singular form of a noun.
-
- Args:
- noun (str): The noun to singularize.
- *pargs: An optional quantity (number, list, dict, or set). When the
- quantity is not ``1`` the original noun is returned unchanged.
-
- Returns:
- str: The singular form of the noun, or the original noun when the
- optional quantity is not ``1``.
- """
-if indefinite_article.__doc__ is None:
- indefinite_article.__doc__ = """Return a noun preceded by the appropriate indefinite article.
-
- Args:
- noun (str): The noun phrase to precede with an article.
- **kwargs: Additional keyword arguments passed to the underlying
- language function.
-
- Returns:
- str: The noun prefixed with ``'a'`` or ``'an'`` as appropriate
- (e.g. ``indefinite_article('apple')`` returns ``'an apple'``).
- """
-if capitalize.__doc__ is None:
- capitalize.__doc__ = """Return the input string with the first letter capitalized.
-
- Args:
- a (str): The string to capitalize.
- **kwargs: Additional keyword arguments passed to the underlying
- language function.
-
- Returns:
- str: The input string with its first character converted to
- upper case.
- """
-if period_list.__doc__ is None:
- period_list.__doc__ = """Return a list of per-year period options for use in multiple-choice fields.
-
- Returns:
- list: A list of ``[number, label]`` pairs representing common
- payment periods (e.g. ``[[12, 'Per Month'], [1, 'Per Year'],
- [52, 'Per Week'], ...]``).
- """
-if name_suffix.__doc__ is None:
- name_suffix.__doc__ = """Return a list of common name suffixes for use in multiple-choice fields.
-
- Returns:
- list: A list of name suffix strings such as
- ``['Jr', 'Sr', 'II', 'III', 'IV', 'V', 'VI']``.
- """
-if currency.__doc__ is None:
- currency.__doc__ = """Format a number as a currency value using the current locale.
-
- Args:
- value: The numeric value to format.
- **kwargs: Optional keyword arguments including ``decimals`` (bool,
- default ``True``), ``symbol`` (str override for the currency
- symbol), and ``symbol_precedes`` (bool controlling symbol
- position).
- Returns:
- str: The formatted currency string (e.g. ``currency(45.2)`` returns
- ``'$45.20'`` for a US locale).
- """
-if currency_symbol.__doc__ is None:
- currency_symbol.__doc__ = """Return the currency symbol for the current locale.
- Returns:
- str: The currency symbol (e.g. ``'$'`` for a US locale). Respects
- overrides set via :func:`set_locale` or the ``currency symbol``
- configuration setting.
- """
-if possessify.__doc__ is None:
- possessify.__doc__ = """Return the possessive phrase combining two arguments.
- Args:
- a: The possessor.
- b: The thing possessed.
- **kwargs: Additional keyword arguments passed to the underlying
- language function.
+# def set_da_config(config):
+# global daconfig
+# daconfig = config
- Returns:
- str: A possessive phrase such as ``"a's b"``.
- """
-if possessify_long.__doc__ is None:
- possessify_long.__doc__ = """Return the long possessive phrase combining two arguments.
+
+def get_config(key, none_value=None):
+ """Return a value from the docassemble configuration file.
Args:
- a: The possessor.
- b: The thing possessed.
- **kwargs: Additional keyword arguments passed to the underlying
- language function.
+ key (str): The configuration directive to look up.
+ none_value (optional): The value to return if the key is not found in
+ the configuration. Defaults to None.
Returns:
- str: A possessive phrase of the form ``"the b of a"``.
+ The configuration value associated with the key, or ``none_value``
+ if the key is not present.
"""
-if comma_list.__doc__ is None:
- comma_list.__doc__ = """Return the items joined by commas.
+ return get_configuration().get(key, none_value)
- Args:
- *pargs: Items to join, or a single iterable as the first argument.
- **kwargs: Optional ``comma_string`` (default ``', '``) to customize
- the separator.
+# def set_default_language(lang):
+# global default_language
+# default_language = lang
- Returns:
- str: The items separated by commas (e.g.
- ``comma_list('lions', 'tigers', 'bears')`` returns
- ``'lions, tigers, bears'``).
- """
-if comma_and_list.__doc__ is None:
- comma_and_list.__doc__ = """Return the items joined by commas with "and" before the last item.
+# def set_default_dialect(dialect):
+# global default_dialect
+# default_dialect = dialect
+# return
- Args:
- *pargs: Items to join, or a single iterable as the first argument.
- **kwargs: Optional keyword arguments including ``oxford`` (bool,
- default ``True``), ``and_string`` (default ``'and'``),
- ``comma_string``, ``before_and``, and ``after_and``.
+# def set_default_country(country):
+# global default_country
+# default_country = country
+# return
- Returns:
- str: An English-language listing such as ``'lions, tigers, and
- bears'``.
- """
-if add_separators.__doc__ is None:
- add_separators.__doc__ = """Return the list items as strings with separators appended.
+# def set_default_timezone(timezone):
+# global default_timezone
+# default_timezone = timezone
+# return
- Appends ``;`` to all items except the penultimate, which gets
- ``'; and'``, and the last, which gets ``'.'``.
- Args:
- the_list: The list of items to process.
- separator (str, optional): Separator appended to middle items.
- Defaults to ``';'``.
- last_separator (str, optional): Separator appended to the
- penultimate item. Defaults to ``'; and'``.
- end_mark (str, optional): Mark appended to the final item.
- Defaults to ``'.'``.
+# def reset_thread_local():
+# this_thread.open_files = set()
+# this_thread.temporary_resources = set()
- Returns:
- list: A list of strings with separators appended.
- """
-if nice_number.__doc__ is None:
- nice_number.__doc__ = """Return a number expressed as a word for small values, or as a formatted numeral.
+# def reset_thread_variables():
+# this_thread.saved_files = {}
+# this_thread.message_log = []
- Args:
- num: The number to convert.
- **kwargs: Optional keyword arguments including ``capitalize``
- (bool), ``language`` (str), and ``use_word`` (bool, default
- ``False``).
- Returns:
- str: The number as a word (e.g. ``nice_number(4)`` returns
- ``'four'``) or as a locale-formatted numeral for larger values.
- """
-if quantity_noun.__doc__ is None:
- quantity_noun.__doc__ = """Return a number combined with a noun in the appropriate singular or plural form.
+# def reset_local_variables():
+# # logmessage("reset_local_variables")
+# this_thread.language = server.default_language
+# this_thread.dialect = server.default_dialect
+# this_thread.voice = server.default_voice
+# this_thread.country = server.default_country
+# this_thread.locale = server.default_locale
+# this_thread.session_id = None
+# this_thread.interview = None
+# this_thread.interview_status = None
+# this_thread.evaluation_context = None
+# this_thread.gathering_mode = {}
+# this_thread.global_vars = GenericObject()
+# this_thread.current_variable = []
+# # this_thread.template_vars = []
+# this_thread.open_files = set()
+# this_thread.saved_files = {}
+# this_thread.message_log = []
+# this_thread.misc = {}
+# this_thread.probing = False
+# this_thread.current_info = {}
+# this_thread.current_package = None
+# this_thread.current_question = None
+# this_thread.current_section = None
+# this_thread.internal = {}
+# this_thread.markdown = markdown.Markdown(extensions=['smarty', 'markdown.extensions.sane_lists', 'markdown.extensions.tables', 'markdown.extensions.attr_list', 'markdown.extensions.md_in_html', 'footnotes'], output_format='html5')
+# this_thread.prevent_going_back = False
- Combines :func:`nice_number` and :func:`noun_plural`. Rounds the number
- to the nearest integer unless ``as_integer=False`` is passed.
- Args:
- num: The quantity.
- noun (str): The singular noun.
- **kwargs: Optional keyword arguments including ``as_integer``
- (bool, default ``True``) and other arguments accepted by
- :func:`nice_number`.
+def prevent_going_back():
+ """Disable the back button so the user cannot revisit previous questions.
- Returns:
- str: The quantity and noun combined (e.g. ``quantity_noun(2,
- 'apple')`` returns ``'two apples'``).
+ Once called, the user will not be able to go back and change any answers
+ entered before this point in the interview.
"""
-if title_case.__doc__ is None:
- title_case.__doc__ = """Return the input string with the first letter of each word capitalized.
+ this_thread.prevent_going_back = True
- Args:
- a (str): The string to convert to title case.
- **kwargs: Additional keyword arguments passed to the underlying
- language function.
- Returns:
- str: The title-cased string (e.g. ``title_case('the importance of
- being ernest')`` returns ``'The Importance of Being Ernest'``).
- """
-if ordinal_number.__doc__ is None:
- ordinal_number.__doc__ = """Return the ordinal form of a cardinal number.
+def manual_line_breaks(text):
+ """Replaces newlines with manual line breaks."""
+ if this_thread.evaluation_context == 'docx':
+ return re.sub(r' *\r?\n *', ' ', str(text))
+ return re.sub(r' *\r?\n *', ' [BR] ', str(text))
- Args:
- num: The cardinal number (1-based).
- **kwargs: Optional keyword arguments including ``capitalize``
- (bool) and ``use_word`` (bool, default depends on the value).
- Returns:
- str: The ordinal form (e.g. ``ordinal_number(8)`` returns
- ``'eighth'``; ``ordinal_number(8, use_word=False)`` returns
- ``'8th'``).
- """
-if ordinal.__doc__ is None:
- ordinal.__doc__ = """Return the ordinal form of a zero-based index.
+def need(*pargs):
+ """Ensure that the given variables are defined, asking questions if necessary.
- Equivalent to ``ordinal_number(num + 1)``. This is useful when working
- with zero-based list indexes.
+ Evaluating each argument causes docassemble to seek its definition
+ through the normal interview logic. The function always returns
+ ``True``. Using ``need()`` is purely for readability; writing
+ ``need(x, y)`` is equivalent to writing ``x; y`` in a code block.
Args:
- num: The zero-based index.
- **kwargs: Optional keyword arguments passed to :func:`ordinal_number`.
+ *pargs: Variables whose definitions should be ensured.
Returns:
- str: The ordinal form (e.g. ``ordinal(0)`` returns ``'first'``;
- ``ordinal(22)`` returns ``'23rd'``).
+ bool: Always ``True``.
"""
+ ensure_definition(*pargs)
+ for argument in pargs:
+ argument # pylint: disable=pointless-statement
+ return True
+
+
+def pickleable_objects(input_dict):
+ output_dict = {}
+ for key in input_dict:
+ if isinstance(input_dict[key], (types.ModuleType, types.FunctionType, TypeType, types.BuiltinFunctionType, types.BuiltinMethodType, types.MethodType, FileType)):
+ continue
+ if key == "__builtins__":
+ continue
+ output_dict[key] = input_dict[key]
+ return output_dict
def underscore_to_space(a):
@@ -4929,10 +3142,10 @@ def store_variables_snapshot(data=None, include_internal=False, key=None, persis
if key is not None and not isinstance(key, str):
raise DAError("store_variables_snapshot: key must be a string")
if data is None:
- the_data = serializable_dict(get_user_dict(), include_internal=include_internal)
+ the_data = serializable_dict(get_current_user_dict(), include_internal=include_internal)
else:
the_data = safe_json(data)
- server.write_answer_json(session, filename, the_data, tags=key, persistent=bool(persistent))
+ write_answer_json(session, filename, the_data, tags=key, persistent=bool(persistent))
def all_variables(simplify=True, include_internal=False, special=False, make_copy=False):
@@ -4960,18 +3173,18 @@ def all_variables(simplify=True, include_internal=False, special=False, make_cop
when ``special='tags'``.
"""
if special == 'titles':
- return this_thread.interview.get_title(get_user_dict(), adapted=True)
+ return this_thread.interview.get_title(get_current_user_dict(), adapted=True)
if special == 'metadata':
return copy.deepcopy(this_thread.interview.consolidated_metadata)
if special == 'tags':
session_tags()
return copy.deepcopy(this_thread.internal['tags'])
if simplify:
- return serializable_dict(get_user_dict(), include_internal=include_internal)
+ return serializable_dict(get_current_user_dict(), include_internal=include_internal)
if make_copy:
- new_dict = copy.deepcopy(pickleable_objects(get_user_dict()))
+ new_dict = copy.deepcopy(pickleable_objects(get_current_user_dict()))
else:
- new_dict = pickleable_objects(get_user_dict())
+ new_dict = pickleable_objects(get_current_user_dict())
if not include_internal and '_internal' in new_dict:
new_dict = copy.copy(new_dict)
del new_dict['_internal']
@@ -5031,7 +3244,7 @@ def force_ask(*pargs, **kwargs):
for item in the_pargs:
if isinstance(item, str) and illegal_variable_name(item):
raise DAError("Illegal variable name")
- raise ForcedNameError(*the_pargs, user_dict=get_user_dict(), evaluate=kwargs.get('evaluate', False))
+ raise ForcedNameError(*the_pargs, user_dict=get_current_user_dict(), evaluate=kwargs.get('evaluate', False))
force_ask_nameerror(the_pargs[0])
@@ -5062,7 +3275,7 @@ def force_gather(*pargs, forget_prior=False, evaluate=False):
unique_id = this_thread.current_info['user']['session_uid']
if 'event_stack' in this_thread.internal and unique_id in this_thread.internal['event_stack']:
this_thread.internal['event_stack'][unique_id] = []
- the_user_dict = get_user_dict()
+ the_user_dict = get_current_user_dict()
the_context = {}
for var_name in ('x', 'i', 'j', 'k', 'l', 'm', 'n'):
if var_name in the_user_dict:
@@ -5088,7 +3301,7 @@ def static_filename_path(filereference, return_nonexistent=False):
else:
result = package_data_filename(static_filename(filereference), return_nonexistent=return_nonexistent)
# if result is None or not os.path.isfile(result):
- # result = server.absolute_filename("/playgroundstatic/" + re.sub(r'[^A-Za-z0-9\-\_\. ]', '', filereference)).path
+ # result = absolute_filename("/playgroundstatic/" + re.sub(r'[^A-Za-z0-9\-\_\. ]', '', filereference)).path
return result
@@ -5189,7 +3402,7 @@ def package_template_filename(the_file, **kwargs):
m = re.search(r'^docassemble\.playground([0-9]+)([A-Za-z]?[A-Za-z0-9]*)$', parts[0])
if m:
parts[1] = re.sub(r'^data/templates/', '', parts[1])
- abs_file = server.absolute_filename("/playgroundtemplate/" + m.group(1) + '/' + (m.group(2) or 'default') + '/' + re.sub(r'[^A-Za-z0-9\-\_\. ]', '', parts[1])) # pylint: disable=assignment-from-none
+ abs_file = absolute_filename("/playgroundtemplate/" + m.group(1) + '/' + (m.group(2) or 'default') + '/' + re.sub(r'[^A-Za-z0-9\-\_\. ]', '', parts[1])) # pylint: disable=assignment-from-none
if abs_file is None:
return None
return abs_file.path
@@ -5236,12 +3449,12 @@ def package_data_filename(the_file, return_nonexistent=False):
if m:
if re.search(r'^data/sources/', parts[1]):
parts[1] = re.sub(r'^data/sources/', '', parts[1])
- abs_file = server.absolute_filename("/playgroundsources/" + m.group(1) + '/' + (m.group(2) or 'default') + '/' + re.sub(r'[^A-Za-z0-9\-\_\. ]', '', parts[1])) # pylint: disable=assignment-from-none
+ abs_file = absolute_filename("/playgroundsources/" + m.group(1) + '/' + (m.group(2) or 'default') + '/' + re.sub(r'[^A-Za-z0-9\-\_\. ]', '', parts[1])) # pylint: disable=assignment-from-none
if abs_file is None:
return None
return abs_file.path
parts[1] = re.sub(r'^data/static/', '', parts[1])
- abs_file = server.absolute_filename("/playgroundstatic/" + m.group(1) + '/' + (m.group(2) or 'default') + '/' + re.sub(r'[^A-Za-z0-9\-\_\. ]', '', parts[1])) # pylint: disable=assignment-from-none
+ abs_file = absolute_filename("/playgroundstatic/" + m.group(1) + '/' + (m.group(2) or 'default') + '/' + re.sub(r'[^A-Za-z0-9\-\_\. ]', '', parts[1])) # pylint: disable=assignment-from-none
if abs_file is None:
return None
return abs_file.path
@@ -5256,7 +3469,7 @@ def package_data_filename(the_file, return_nonexistent=False):
else:
result = None
# if result is None or not os.path.isfile(result):
- # result = server.absolute_filename("/playgroundstatic/" + re.sub(r'[^A-Za-z0-9\-\_\.]', '', the_file)).path
+ # result = absolute_filename("/playgroundstatic/" + re.sub(r'[^A-Za-z0-9\-\_\.]', '', the_file)).path
return result
@@ -5336,7 +3549,7 @@ def process_action():
else:
# logmessage("process_action: doing a gather of " + variable_name)
if len(variable_dict['context']) > 0:
- the_user_dict = get_user_dict()
+ the_user_dict = get_current_user_dict()
for var_name, var_val in variable_dict['context'].items():
the_user_dict[var_name] = var_val
del the_user_dict
@@ -5351,7 +3564,7 @@ def process_action():
this_thread.current_info.update(event_info)
the_context = event_info.get('context', {})
if len(the_context) > 0:
- the_user_dict = get_user_dict()
+ the_user_dict = get_current_user_dict()
for var_name, var_val in the_context.items():
the_user_dict[var_name] = var_val
del the_user_dict
@@ -5406,7 +3619,7 @@ def process_action():
for variable_name in this_thread.current_info['arguments']['variables']:
if variable_name not in [(variable_dict if isinstance(variable_dict, str) else variable_dict['var']) for variable_dict in this_thread.internal['gather']]:
the_context = {}
- the_user_dict = get_user_dict()
+ the_user_dict = get_current_user_dict()
for var_name in ('x', 'i', 'j', 'k', 'l', 'm', 'n'):
if var_name in the_user_dict:
the_context[var_name] = the_user_dict[var_name]
@@ -5616,7 +3829,7 @@ def process_action():
for var in this_thread.current_info['arguments'][key]:
if var not in [(variable_dict if isinstance(variable_dict, str) else variable_dict['var']) for variable_dict in this_thread.internal['gather']]:
the_context = {}
- the_user_dict = get_user_dict()
+ the_user_dict = get_current_user_dict()
for var_name in ('x', 'i', 'j', 'k', 'l', 'm', 'n'):
if var_name in the_user_dict:
the_context[var_name] = the_user_dict[var_name]
@@ -5624,7 +3837,7 @@ def process_action():
this_thread.internal['gather'].append({'var': var, 'context': the_context})
elif this_thread.current_info['arguments'][key] not in [(variable_dict if isinstance(variable_dict, str) else variable_dict['var']) for variable_dict in this_thread.internal['gather']]:
the_context = {}
- the_user_dict = get_user_dict()
+ the_user_dict = get_current_user_dict()
for var_name in ('x', 'i', 'j', 'k', 'l', 'm', 'n'):
if var_name in the_user_dict:
the_context[var_name] = the_user_dict[var_name]
@@ -5689,7 +3902,7 @@ def myb64unquote(text):
def debug_status():
- return server.debug
+ return get_debug_status()
# grep -E -R -o -h "word\(['\"][^\)]+\)" * | sed "s/^[^'\"]+['\"]//g"
@@ -5743,20 +3956,20 @@ def repad_byte(text):
return text + (equals_byte * ((4 - len(text) % 4) % 4))
-class lister(ast.NodeVisitor):
+class Lister(ast.NodeVisitor):
def __init__(self):
self.stack = []
- def visit_Name(self, node):
+ def visit_Name(self, node): # pylint: disable=invalid-name
self.stack.append(['name', node.id])
ast.NodeVisitor.generic_visit(self, node)
- def visit_Attribute(self, node):
+ def visit_Attribute(self, node): # pylint: disable=invalid-name
self.stack.append(['attr', node.attr])
ast.NodeVisitor.generic_visit(self, node)
- def visit_Subscript(self, node):
+ def visit_Subscript(self, node): # pylint: disable=invalid-name
self.stack.append(['index', re.sub(r'\n', '', astunparse.unparse(node.slice))])
ast.NodeVisitor.generic_visit(self, node)
# def visit_BinOp(self, node):
@@ -5768,7 +3981,7 @@ def visit_Subscript(self, node):
def components_of(full_variable):
node = ast.parse(full_variable, mode='eval')
- crawler = lister()
+ crawler = Lister()
crawler.visit(node)
components = list(reversed(crawler.stack))
start_index = 0
@@ -5778,18 +3991,8 @@ def components_of(full_variable):
return components[start_index:]
-def get_user_dict():
- frame = sys._getframe(1)
- while frame is not None:
- f_locals = frame.f_locals
- if 'user_dict' in f_locals:
- user_dict = f_locals['user_dict']
- if isinstance(user_dict, dict) and '_internal' in user_dict:
- return user_dict
- if '_internal' in f_locals:
- return f_locals
- frame = frame.f_back
- return {}
+get_user_dict = get_current_user_dict
+
def invalidate(*pargs):
"""Make one or more variables undefined while remembering their prior values as defaults.
@@ -5820,18 +4023,7 @@ def _undefine_internal_old(*pargs, invalidate=False): # pylint: disable=redefin
raise DAError("undefine: variable " + repr(var) + " is not a valid variable name")
if len(vars_to_delete) == 0:
return
- frame = sys._getframe(1)
- the_user_dict = frame.f_locals
- while '_internal' not in the_user_dict:
- frame = frame.f_back
- if frame is None:
- return
- if 'user_dict' in frame.f_locals:
- the_user_dict = frame.f_locals['user_dict']
- if '_internal' in the_user_dict:
- break
- return
- the_user_dict = frame.f_locals
+ the_user_dict = get_current_user_dict()
this_thread.probing = True
if invalidate:
for var in vars_to_delete:
@@ -5847,58 +4039,59 @@ def _undefine_internal_old(*pargs, invalidate=False): # pylint: disable=redefin
this_thread.probing = False
-def _undefine_internal_new(*pargs, invalidate=False): # pylint: disable=redefined-outer-name
- vars_to_delete = []
- the_pargs = unpack_pargs(pargs)
- for var in the_pargs:
- str(var)
- if not isinstance(var, str):
- raise DAError("undefine() must be given a string, not " + repr(var) + ", a " + str(var.__class__.__name__))
- try:
- eval(var, {})
- continue
- except:
- vars_to_delete.append(var)
- components = components_of(var)
- if len(components) == 0 or len(components[0]) < 2:
- raise DAError("undefine: variable " + repr(var) + " is not a valid variable name")
- if len(vars_to_delete) == 0:
- return
- frame = sys._getframe(1)
- the_user_dict = frame.f_locals
- the_user_dict_g = frame.f_globals
- while '_internal' not in the_user_dict:
- frame = frame.f_back
- if frame is None:
- return
- if 'user_dict' in frame.f_locals:
- the_user_dict = frame.f_locals['user_dict']
- the_user_dict_g = frame.f_globals
- if '_internal' in the_user_dict:
- break
- return
- the_user_dict = frame.f_locals
- the_user_dict_g = frame.f_globals
- this_thread.probing = True
- if invalidate:
- for var in vars_to_delete:
- try:
- exec("_internal['dirty'][" + repr(var) + "] = " + var, the_user_dict_g, the_user_dict)
- except:
- pass
- for var in vars_to_delete:
- try:
- exec('del ' + var, the_user_dict_g, the_user_dict)
- except:
- pass
- this_thread.probing = False
-
-
-if python313:
- _undefine_internal = _undefine_internal_new
-else:
- _undefine_internal = _undefine_internal_old
-
+# def _undefine_internal_new(*pargs, invalidate=False): # pylint: disable=redefined-outer-name
+# vars_to_delete = []
+# the_pargs = unpack_pargs(pargs)
+# for var in the_pargs:
+# str(var)
+# if not isinstance(var, str):
+# raise DAError("undefine() must be given a string, not " + repr(var) + ", a " + str(var.__class__.__name__))
+# try:
+# eval(var, {})
+# continue
+# except:
+# vars_to_delete.append(var)
+# components = components_of(var)
+# if len(components) == 0 or len(components[0]) < 2:
+# raise DAError("undefine: variable " + repr(var) + " is not a valid variable name")
+# if len(vars_to_delete) == 0:
+# return
+# frame = sys._getframe(1)
+# the_user_dict = frame.f_locals
+# the_user_dict_g = frame.f_globals
+# while '_internal' not in the_user_dict:
+# frame = frame.f_back
+# if frame is None:
+# return
+# if 'user_dict' in frame.f_locals:
+# the_user_dict = frame.f_locals['user_dict']
+# the_user_dict_g = frame.f_globals
+# if '_internal' in the_user_dict:
+# break
+# return
+# the_user_dict = frame.f_locals
+# the_user_dict_g = frame.f_globals
+# this_thread.probing = True
+# if invalidate:
+# for var in vars_to_delete:
+# try:
+# exec("_internal['dirty'][" + repr(var) + "] = " + var, the_user_dict_g, the_user_dict)
+# except:
+# pass
+# for var in vars_to_delete:
+# try:
+# exec('del ' + var, the_user_dict_g, the_user_dict)
+# except:
+# pass
+# this_thread.probing = False
+
+
+# if python313:
+# _undefine_internal = _undefine_internal_new
+# else:
+# _undefine_internal = _undefine_internal_old
+
+_undefine_internal = _undefine_internal_old
def undefine(*pargs, invalidate=False): # pylint: disable=redefined-outer-name
"""Delete one or more interview variables, making them undefined.
@@ -5960,11 +4153,11 @@ def set_variables(variables, process_objects=False):
variables = variables.elements
if not isinstance(variables, dict):
raise DAError("set_variables: argument must be a dictionary")
- user_dict = get_user_dict()
+ user_dict = get_current_user_dict()
if user_dict is None:
raise DAError("set_variables: could not find interview answers")
if process_objects:
- variables = server.transform_json_variables(variables) # pylint: disable=assignment-from-none
+ variables = transform_json_variables(variables) # pylint: disable=assignment-from-none
for var, val in variables.items():
exec(var + " = None", user_dict)
user_dict['__define_val'] = val
@@ -5987,16 +4180,15 @@ def define(var, val):
ensure_definition(var, val)
if not isinstance(var, str) or not re.search(r'^[A-Za-z_]', var):
raise DAError("define() must be given a string as the variable name")
- user_dict = get_user_dict()
+ user_dict = get_current_user_dict()
if user_dict is None:
raise DAError("define: could not find interview answers")
- # Trigger exceptions for the left hand side before creating __define_val
- exec(var + " = None", user_dict)
- # logmessage("Got past the lhs check")
user_dict['__define_val'] = val
- exec(var + " = __define_val", user_dict)
- if '__define_val' in user_dict:
- del user_dict['__define_val']
+ try:
+ exec(var + " = __define_val", user_dict)
+ finally:
+ if '__define_val' in user_dict:
+ del user_dict['__define_val']
class DefCaller(Enum):
@@ -6014,15 +4206,15 @@ def is_predicate(self) -> bool:
return self == self.DEFINED
-def _defined_internal_with_prior(var, caller: DefCaller, alt=None):
+def _inspect_user_dict_with_prior(var, caller: DefCaller, alt=None):
try:
- return _defined_internal(var, caller, alt=alt, prior=True)
+ return _inspect_user_dict(var, caller, alt=alt, prior=True)
except:
- return _defined_internal(var, caller, alt=alt)
+ return _inspect_user_dict(var, caller, alt=alt)
-def _defined_internal_old(var, caller: DefCaller, alt=None, prior=False):
- """Checks if a variable is defined at all in the stack. Used by defined(),
+def _inspect_user_dict(var, caller: DefCaller, alt=None, prior=False):
+ """Checks if a variable is defined. Used by defined(),
value(), and showifdef(). `var` is the name of the variable to check,
`caller` is the name of the function calling (which determines what to do
if the variable is found to be defined or not).
@@ -6033,30 +4225,13 @@ def _defined_internal_old(var, caller: DefCaller, alt=None, prior=False):
user all of the questions necessary to answer it
* SHOWIFDEF, then the value if returned, but only if no questions have to be asked
"""
- frame = sys._getframe(1)
components = components_of(var)
if len(components) == 0 or len(components[0]) < 2:
raise DAError("defined: variable " + repr(var) + " is not a valid variable name")
variable = components[0][1]
- the_user_dict = frame.f_locals
+ the_user_dict = get_old_user_dict() if prior else get_current_user_dict()
failure_val = False if caller.is_predicate() else alt
- user_dict_name = 'old_user_dict' if prior else 'user_dict'
- while (variable not in the_user_dict) or prior:
- frame = frame.f_back
- if frame is None:
- if caller.is_pure():
- return failure_val
- force_ask_nameerror(variable)
- if user_dict_name in frame.f_locals:
- the_user_dict = frame.f_locals[user_dict_name]
- if variable in the_user_dict:
- break
- if caller.is_pure():
- return failure_val
- force_ask_nameerror(variable)
- else:
- the_user_dict = frame.f_locals
- if variable not in the_user_dict:
+ if the_user_dict is None or variable not in the_user_dict:
if caller.is_pure():
return failure_val
force_ask_nameerror(variable)
@@ -6149,142 +4324,141 @@ def _defined_internal_old(var, caller: DefCaller, alt=None, prior=False):
return eval(cum_variable, the_user_dict)
-def _defined_internal_new(var, caller: DefCaller, alt=None, prior=False):
- """Checks if a variable is defined at all in the stack. Used by defined(),
- value(), and showifdef(). `var` is the name of the variable to check,
- `caller` is the name of the function calling (which determines what to do
- if the variable is found to be defined or not).
-
- if caller is:
- * DEFINED, then True/False is returned depending on if the variable is defined
- * VALUE, then the actual value of the variable is returned, after asking the
- user all of the questions necessary to answer it
- * SHOWIFDEF, then the value if returned, but only if no questions have to be asked
- """
- frame = sys._getframe(1)
- components = components_of(var)
- if len(components) == 0 or len(components[0]) < 2:
- raise DAError("defined: variable " + repr(var) + " is not a valid variable name")
- variable = components[0][1]
- the_user_dict = frame.f_locals
- the_user_dict_g = frame.f_globals
- failure_val = False if caller.is_predicate() else alt
- user_dict_name = 'old_user_dict' if prior else 'user_dict'
- while (variable not in the_user_dict) or prior:
- frame = frame.f_back
- if frame is None:
- if caller.is_pure():
- return failure_val
- force_ask_nameerror(variable)
- if user_dict_name in frame.f_locals:
- the_user_dict = frame.f_locals[user_dict_name]
- the_user_dict_g = frame.f_globals
- if variable in the_user_dict:
- break
- if caller.is_pure():
- return failure_val
- force_ask_nameerror(variable)
- else:
- the_user_dict = frame.f_locals
- the_user_dict_g = frame.f_globals
- if variable not in the_user_dict:
- if caller.is_pure():
- return failure_val
- force_ask_nameerror(variable)
- if len(components) == 1:
- if caller.is_predicate():
- return True
- return eval(variable, the_user_dict_g, the_user_dict)
- cum_variable = ''
- if caller.is_pure():
- this_thread.probing = True
- has_random_instance_name = False
- for elem in components:
- if elem[0] == 'name':
- cum_variable = elem[1]
- continue
- if elem[0] == 'attr':
- base_var = cum_variable
- to_eval = "hasattr(" + cum_variable + ", " + repr(elem[1]) + ")"
- cum_variable += '.' + elem[1]
- try:
- result = eval(to_eval, the_user_dict_g, the_user_dict)
- except:
- if caller.is_pure():
- this_thread.probing = False
- return failure_val
- force_ask_nameerror(base_var)
- if result:
- continue
- if caller.is_pure():
- this_thread.probing = False
- return failure_val
- the_cum = eval(base_var, the_user_dict_g, the_user_dict)
- try:
- if not the_cum.has_nonrandom_instance_name:
- has_random_instance_name = True
- except:
- pass
- if has_random_instance_name:
- force_ask_nameerror(cum_variable)
- getattr(the_cum, elem[1])
- elif elem[0] == 'index':
- try:
- the_index = eval(elem[1], the_user_dict_g, the_user_dict)
- except:
- if caller.is_pure():
- this_thread.probing = False
- return failure_val
- value(elem[1])
- try:
- the_cum = eval(cum_variable, the_user_dict_g, the_user_dict)
- except:
- if caller.is_pure():
- this_thread.probing = False
- return failure_val
- force_ask_nameerror(cum_variable)
- if hasattr(the_cum, 'instanceName') and hasattr(the_cum, 'elements'):
- var_elements = cum_variable + '.elements'
- else:
- var_elements = cum_variable
- if isinstance(the_index, int):
- to_eval = 'len(' + var_elements + ') > ' + str(the_index)
- else:
- to_eval = elem[1] + " in " + var_elements
- cum_variable += '[' + elem[1] + ']'
- try:
- result = eval(to_eval, the_user_dict_g, the_user_dict)
- except:
- # the evaluation probably will never fail because we know the base variable is defined
- if caller.is_pure():
- this_thread.probing = False
- return failure_val
- force_ask_nameerror(cum_variable)
- if result:
- continue
- if caller.is_pure():
- this_thread.probing = False
- return failure_val
- try:
- if not the_cum.has_nonrandom_instance_name:
- has_random_instance_name = True
- except:
- pass
- if has_random_instance_name:
- force_ask_nameerror(cum_variable)
- the_cum[the_index] # pylint: disable=pointless-statement
- if caller.is_pure():
- this_thread.probing = False
- if caller.is_predicate():
- return True
- return eval(cum_variable, the_user_dict_g, the_user_dict)
-
-
-if python313:
- _defined_internal = _defined_internal_new
-else:
- _defined_internal = _defined_internal_old
+# def _inspect_user_dict_new(var, caller: DefCaller, alt=None, prior=False):
+# """Checks if a variable is defined at all in the stack. Used by defined(),
+# value(), and showifdef(). `var` is the name of the variable to check,
+# `caller` is the name of the function calling (which determines what to do
+# if the variable is found to be defined or not).
+# if caller is:
+# * DEFINED, then True/False is returned depending on if the variable is defined
+# * VALUE, then the actual value of the variable is returned, after asking the
+# user all of the questions necessary to answer it
+# * SHOWIFDEF, then the value if returned, but only if no questions have to be asked
+# """
+# frame = sys._getframe(1)
+# components = components_of(var)
+# if len(components) == 0 or len(components[0]) < 2:
+# raise DAError("defined: variable " + repr(var) + " is not a valid variable name")
+# variable = components[0][1]
+# the_user_dict = frame.f_locals
+# the_user_dict_g = frame.f_globals
+# failure_val = False if caller.is_predicate() else alt
+# user_dict_name = 'old_user_dict' if prior else 'user_dict'
+# while (variable not in the_user_dict) or prior:
+# frame = frame.f_back
+# if frame is None:
+# if caller.is_pure():
+# return failure_val
+# force_ask_nameerror(variable)
+# if user_dict_name in frame.f_locals:
+# the_user_dict = frame.f_locals[user_dict_name]
+# the_user_dict_g = frame.f_globals
+# if variable in the_user_dict:
+# break
+# if caller.is_pure():
+# return failure_val
+# force_ask_nameerror(variable)
+# else:
+# the_user_dict = frame.f_locals
+# the_user_dict_g = frame.f_globals
+# if variable not in the_user_dict:
+# if caller.is_pure():
+# return failure_val
+# force_ask_nameerror(variable)
+# if len(components) == 1:
+# if caller.is_predicate():
+# return True
+# return eval(variable, the_user_dict_g, the_user_dict)
+# cum_variable = ''
+# if caller.is_pure():
+# this_thread.probing = True
+# has_random_instance_name = False
+# for elem in components:
+# if elem[0] == 'name':
+# cum_variable = elem[1]
+# continue
+# if elem[0] == 'attr':
+# base_var = cum_variable
+# to_eval = "hasattr(" + cum_variable + ", " + repr(elem[1]) + ")"
+# cum_variable += '.' + elem[1]
+# try:
+# result = eval(to_eval, the_user_dict_g, the_user_dict)
+# except:
+# if caller.is_pure():
+# this_thread.probing = False
+# return failure_val
+# force_ask_nameerror(base_var)
+# if result:
+# continue
+# if caller.is_pure():
+# this_thread.probing = False
+# return failure_val
+# the_cum = eval(base_var, the_user_dict_g, the_user_dict)
+# try:
+# if not the_cum.has_nonrandom_instance_name:
+# has_random_instance_name = True
+# except:
+# pass
+# if has_random_instance_name:
+# force_ask_nameerror(cum_variable)
+# getattr(the_cum, elem[1])
+# elif elem[0] == 'index':
+# try:
+# the_index = eval(elem[1], the_user_dict_g, the_user_dict)
+# except:
+# if caller.is_pure():
+# this_thread.probing = False
+# return failure_val
+# value(elem[1])
+# try:
+# the_cum = eval(cum_variable, the_user_dict_g, the_user_dict)
+# except:
+# if caller.is_pure():
+# this_thread.probing = False
+# return failure_val
+# force_ask_nameerror(cum_variable)
+# if hasattr(the_cum, 'instanceName') and hasattr(the_cum, 'elements'):
+# var_elements = cum_variable + '.elements'
+# else:
+# var_elements = cum_variable
+# if isinstance(the_index, int):
+# to_eval = 'len(' + var_elements + ') > ' + str(the_index)
+# else:
+# to_eval = elem[1] + " in " + var_elements
+# cum_variable += '[' + elem[1] + ']'
+# try:
+# result = eval(to_eval, the_user_dict_g, the_user_dict)
+# except:
+# # the evaluation probably will never fail because we know the base variable is defined
+# if caller.is_pure():
+# this_thread.probing = False
+# return failure_val
+# force_ask_nameerror(cum_variable)
+# if result:
+# continue
+# if caller.is_pure():
+# this_thread.probing = False
+# return failure_val
+# try:
+# if not the_cum.has_nonrandom_instance_name:
+# has_random_instance_name = True
+# except:
+# pass
+# if has_random_instance_name:
+# force_ask_nameerror(cum_variable)
+# the_cum[the_index] # pylint: disable=pointless-statement
+# if caller.is_pure():
+# this_thread.probing = False
+# if caller.is_predicate():
+# return True
+# return eval(cum_variable, the_user_dict_g, the_user_dict)
+
+
+# if python313:
+# _inspect_user_dict = _inspect_user_dict_new
+# else:
+# _inspect_user_dict = _inspect_user_dict_old
def value(var: str, prior=False):
"""Return the value of an interview variable specified by name.
@@ -6312,8 +4486,8 @@ def value(var: str, prior=False):
if re.search(r'[\(\)\n\r]|lambda:|lambda ', var):
raise DAError("value() is invalid: " + repr(var))
if prior:
- return _defined_internal_with_prior(var, DefCaller.VALUE)
- return _defined_internal(var, DefCaller.VALUE)
+ return _inspect_user_dict_with_prior(var, DefCaller.VALUE)
+ return _inspect_user_dict(var, DefCaller.VALUE)
def defined(var: str, prior=False) -> bool:
@@ -6342,8 +4516,8 @@ def defined(var: str, prior=False) -> bool:
except:
pass
if prior:
- return _defined_internal_with_prior(var, DefCaller.VALUE)
- return _defined_internal(var, DefCaller.DEFINED)
+ return _inspect_user_dict_with_prior(var, DefCaller.VALUE)
+ return _inspect_user_dict(var, DefCaller.DEFINED)
def showifdef(var: str, alternative='', prior=False):
@@ -6374,8 +4548,8 @@ def showifdef(var: str, alternative='', prior=False):
if re.search(r'[\(\)\n\r]|lambda:|lambda ', var):
raise DAError("showifdef() is invalid: " + repr(var))
if prior:
- return _defined_internal_with_prior(var, DefCaller.SHOWIFDEF, alt=alternative)
- return _defined_internal(var, DefCaller.SHOWIFDEF, alt=alternative, prior=prior)
+ return _inspect_user_dict_with_prior(var, DefCaller.SHOWIFDEF, alt=alternative)
+ return _inspect_user_dict(var, DefCaller.SHOWIFDEF, alt=alternative, prior=prior)
def illegal_variable_name(var):
@@ -6385,7 +4559,7 @@ def illegal_variable_name(var):
t = ast.parse(var)
except:
return True
- detector = docassemble.base.astparser.detectIllegal()
+ detector = docassemble.base.astparser.DetectIllegal()
detector.visit(t)
return detector.illegal
@@ -6697,12 +4871,12 @@ def referring_url(default=None, current=False):
URL.
"""
if current:
- url = server.get_referer()
+ url = get_referer()
else:
url = this_thread.internal.get('referer', None)
if url is None:
if default is None:
- default = server.daconfig.get('exitpage', 'https://docassemble.org')
+ default = get_configuration().get('exitpage', 'https://docassemble.org')
url = default
return url
@@ -6790,11 +4964,6 @@ def italic(text, default=None):
return '_' + str(default) + '_'
return '_' + re.sub(r'\_', '', str(text)) + '_'
-# def inspector():
-# frame = inspect.stack()[1][0]
-# for key in frame.__dict__.keys():
-# logmessage(str(key))
-
def indent(text, by=None):
"""Indent each line of the text by a number of spaces.
@@ -7064,11 +5233,11 @@ def interview_list(exclude_invalid=True, action=None, filename=None, session=Non
raise DAError("interview_list: invalid next_id.")
else:
start_id = None
- (the_list, start_id) = server.user_interviews(user_id=user_id, secret=this_thread.current_info['secret'], exclude_invalid=exclude_invalid, action=action, filename=filename, session=session, include_dict=include_dict, delete_shared=delete_shared, start_id=start_id, query=query) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ (the_list, start_id) = user_interviews(user_id=user_id, secret=this_thread.current_info['secret'], exclude_invalid=exclude_invalid, action=action, filename=filename, session=session, include_dict=include_dict, delete_shared=delete_shared, start_id=start_id, query=query) # pylint: disable=assignment-from-none,unpacking-non-sequence
if start_id is None:
return (the_list, None)
return (the_list, myb64quote(str(start_id)))
- return server.user_interviews(user_id=user_id, secret=this_thread.current_info['secret'], exclude_invalid=exclude_invalid, action=action, filename=filename, session=session, include_dict=include_dict, delete_shared=delete_shared, query=query)
+ return user_interviews(user_id=user_id, secret=this_thread.current_info['secret'], exclude_invalid=exclude_invalid, action=action, filename=filename, session=session, include_dict=include_dict, delete_shared=delete_shared, query=query)
return None
@@ -7080,7 +5249,7 @@ def interview_menu(*pargs, **kwargs):
keys such as ``title``, ``filename``, ``link``, ``tags``,
and ``metadata``.
"""
- return server.interview_menu(*pargs, **kwargs)
+ return server_interview_menu(*pargs, **kwargs)
def get_user_list(include_inactive=False, next_id=None):
@@ -7106,7 +5275,7 @@ def get_user_list(include_inactive=False, next_id=None):
raise DAError("get_user_list: invalid next_id.")
else:
start_id = None
- (the_list, start_id) = server.get_user_list(include_inactive=include_inactive, start_id=start_id) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ (the_list, start_id) = server_get_user_list(include_inactive=include_inactive, start_id=start_id) # pylint: disable=assignment-from-none,unpacking-non-sequence
if start_id is None:
return (the_list, None)
return (the_list, myb64quote(str(start_id)))
@@ -7137,19 +5306,19 @@ def manage_privileges(*pargs):
else:
the_command = arglist.pop(0)
if the_command == 'list':
- return server.get_privileges_list()
+ return get_privileges_list()
if the_command == 'inspect':
if len(arglist) != 1:
raise DAError("manage_privileges: invalid number of arguments")
- return server.get_permissions_of_privilege(arglist[0])
+ return get_permissions_of_privilege(arglist[0])
if the_command == 'add':
for priv in arglist:
- server.add_privilege(priv)
+ add_privilege(priv)
if len(arglist) > 0:
return True
elif the_command == 'remove':
for priv in arglist:
- server.remove_privilege(priv)
+ remove_privilege(priv)
if len(arglist) > 0:
return True
else:
@@ -7175,7 +5344,7 @@ def get_user_info(user_id=None, email=None):
"""
if this_thread.current_info['user']['is_authenticated'] and user_id is None and email is None:
user_id = this_thread.current_info['user']['the_user_id']
- return server.get_user_info(user_id=user_id, email=email)
+ return server_get_user_info(user_id=user_id, email=email)
def set_user_info(**kwargs):
@@ -7193,7 +5362,7 @@ def set_user_info(**kwargs):
"""
user_id = kwargs.get('user_id', None)
email = kwargs.get('email', None)
- server.set_user_info(**kwargs)
+ server_set_user_info(**kwargs)
if 'privileges' in kwargs and isinstance(kwargs['privileges'], (list, tuple)) and len(kwargs['privileges']) > 0:
this_thread.current_info['user']['roles'] = list(kwargs['privileges'])
if (user_id is None and email is None) or (user_id is not None and user_id == this_thread.current_info['user']['theid']) or (email is not None and email == this_thread.current_info['user']['email']):
@@ -7222,7 +5391,7 @@ def create_user(email, password, privileges=None, info=None):
Returns:
int: The user ID of the newly created account.
"""
- return server.create_user(email, password, privileges=privileges, info=info)
+ return server_create_user(email, password, privileges=privileges, info=info)
def invite_user(email_address, privilege=None, send=True):
@@ -7243,7 +5412,7 @@ def invite_user(email_address, privilege=None, send=True):
str or None: The registration URL when ``send=False``, otherwise
``None``.
"""
- return server.invite_user(email_address, privilege=privilege, send=send)
+ return server_invite_user(email_address, privilege=privilege, send=send)
def get_user_secret(username, password):
@@ -7261,7 +5430,7 @@ def get_user_secret(username, password):
str or None: The decryption key string if the credentials are valid,
otherwise ``None``.
"""
- return server.get_secret(username, password)
+ return server_get_secret(username, password)
def create_session(yaml_filename, secret=None, url_args=None):
@@ -7280,7 +5449,7 @@ def create_session(yaml_filename, secret=None, url_args=None):
"""
if secret is None:
secret = this_thread.current_info.get('secret', None)
- (encrypted, session_id) = server.create_session(yaml_filename, secret, url_args=url_args) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ (encrypted, session_id) = server_create_session(yaml_filename, secret, url_args=url_args) # pylint: disable=assignment-from-none,unpacking-non-sequence
if secret is None and encrypted:
raise DAError("create_session: the interview is encrypted but you did not provide a secret.")
return session_id
@@ -7306,7 +5475,7 @@ def get_session_variables(yaml_filename, session_id, secret=None, simplify=True)
raise DAError("You cannot get variables from the current interview session")
if secret is None:
secret = this_thread.current_info.get('secret', None)
- return server.get_session_variables(yaml_filename, session_id, secret=secret, simplify=simplify)
+ return server_get_session_variables(yaml_filename, session_id, secret=secret, simplify=simplify)
def set_session_variables(yaml_filename, session_id, variables, secret=None, question_name=None, overwrite=False, process_objects=False, delete=None):
@@ -7339,7 +5508,7 @@ def set_session_variables(yaml_filename, session_id, variables, secret=None, que
delete = [delete]
else:
delete = list(delete)
- server.set_session_variables(yaml_filename, session_id, variables, secret=secret, del_variables=delete, question_name=question_name, post_setting=not overwrite, process_objects=process_objects)
+ server_set_session_variables(yaml_filename, session_id, variables, secret=secret, del_variables=delete, question_name=question_name, post_setting=not overwrite, process_objects=process_objects)
def run_action_in_session(yaml_filename, session_id, action, arguments=None, secret=None, persistent=False, overwrite=False, read_only=False):
@@ -7372,7 +5541,7 @@ def run_action_in_session(yaml_filename, session_id, action, arguments=None, sec
arguments = {}
if secret is None:
secret = this_thread.current_info.get('secret', None)
- result = server.run_action_in_session(i=yaml_filename, session=session_id, secret=secret, action=action, persistent=persistent, overwrite=overwrite, read_only=read_only, arguments=arguments)
+ result = server_run_action_in_session(i=yaml_filename, session=session_id, secret=secret, action=action, persistent=persistent, overwrite=overwrite, read_only=read_only, arguments=arguments)
if isinstance(result, dict):
if result['status'] == 'success':
return True
@@ -7398,7 +5567,7 @@ def get_question_data(yaml_filename, session_id, secret=None):
raise DAError("You cannot get question data from the current interview session")
if secret is None:
secret = this_thread.current_info.get('secret', None)
- return server.get_question_data(yaml_filename, session_id, secret)
+ return server_get_question_data(yaml_filename, session_id, secret)
def go_back_in_session(yaml_filename, session_id, secret=None):
@@ -7416,7 +5585,7 @@ def go_back_in_session(yaml_filename, session_id, secret=None):
raise DAError("You cannot go back in the current interview session")
if secret is None:
secret = this_thread.current_info.get('secret', None)
- server.go_back_in_session(yaml_filename, session_id, secret=secret)
+ server_go_back_in_session(yaml_filename, session_id, secret=secret)
def turn_to_at_sign(match):
@@ -7489,15 +5658,6 @@ def redact(text):
return output
-def ensure_definition(*pargs, **kwargs):
- for val in pargs:
- if isinstance(val, Undefined):
- str(val)
- for val in kwargs.values():
- if isinstance(val, Undefined):
- str(val)
-
-
def verbatim(text):
"""Return the text with special formatting characters escaped for the current output context.
@@ -7580,7 +5740,7 @@ def re_run_logic():
def intrinsic_name_of(var_name, the_user_dict=None):
if the_user_dict is None:
- the_user_dict = get_user_dict()
+ the_user_dict = get_current_user_dict()
from docassemble.base.util import DAObject # pylint: disable=import-outside-toplevel
expression_as_list = [x for x in match_brackets_or_dot.split(var_name) if x != '']
n = len(expression_as_list)
@@ -7600,7 +5760,7 @@ def intrinsic_name_of(var_name, the_user_dict=None):
def intrinsic_names_of(*pargs, the_user_dict=None):
if the_user_dict is None:
- the_user_dict = get_user_dict()
+ the_user_dict = get_current_user_dict()
output = []
for parg in pargs:
if isinstance(parg, str):
@@ -7762,10 +5922,15 @@ def empty(cls):
class ServerContext:
- pass
+ """Class with one attribute, context, which indicates whether the web server or the websockets server is running"""
+ def __init__(self, context):
+ self.context = context
+
+ def set_context(self, context):
+ self.context = context
+
-server_context = ServerContext()
-server_context.context = 'web'
+server_context = ServerContext('web')
def get_action_stack():
diff --git a/docassemble_base/docassemble/base/geocode.py b/docassemble_base/docassemble/base/geocode.py
index 3a0f8225c..084260174 100644
--- a/docassemble_base/docassemble/base/geocode.py
+++ b/docassemble_base/docassemble/base/geocode.py
@@ -1,13 +1,11 @@
-from docassemble.base.logger import logmessage
from geopy.geocoders import GoogleV3
from geopy.geocoders import AzureMaps
+from docassemble.base.logger import logmessage
+from docassemble.base.config import daconfig
class GeoCoder:
- def __init__(self, *pargs, **kwargs): # pylint: disable=unused-argument
- self.server = kwargs['server']
-
def geocode(self, *pargs, **kwargs):
self.data = self.geocoder.geocode(*pargs, **kwargs)
return True
@@ -17,14 +15,14 @@ class GoogleV3GeoCoder(GeoCoder):
def config_ok(self):
try:
- assert isinstance(self.server.daconfig['google']['api key'], str)
+ assert isinstance(daconfig['google']['api key'], str)
except:
logmessage("geocode: cannot geocode without an 'api key' under 'google' in the Configuration. Set 'geolocate service' in the Configuration to use a different geocoding service.")
return False
return True
def initialize(self):
- self.geocoder = GoogleV3(api_key=self.server.daconfig['google']['api key'])
+ self.geocoder = GoogleV3(api_key=daconfig['google']['api key'])
def populate_address(self, address):
if 'formatted_address' in self.data.raw:
@@ -145,14 +143,14 @@ class AzureMapsGeoCoder(GeoCoder):
def config_ok(self):
try:
- assert isinstance(self.server.daconfig['azure maps']['primary key'], str)
+ assert isinstance(daconfig['azure maps']['primary key'], str)
except:
logmessage("geocode: cannot geocode without a 'primary key' under 'azure maps' in the Configuration. Set 'geolocate service' in the Configuration to use a different geocoding service.")
return False
return True
def initialize(self):
- self.geocoder = AzureMaps(self.server.daconfig['azure maps']['primary key'])
+ self.geocoder = AzureMaps(daconfig['azure maps']['primary key'])
def populate_address(self, address):
if 'address' not in self.data.raw:
diff --git a/docassemble_base/docassemble/base/helpers.py b/docassemble_base/docassemble/base/helpers.py
new file mode 100644
index 000000000..dc9d6379d
--- /dev/null
+++ b/docassemble_base/docassemble/base/helpers.py
@@ -0,0 +1,29 @@
+import re
+
+nameerror_match = re.compile(r'\'(.*)\' (is not defined|referenced before assignment|is undefined|where it is not)')
+
+def extract_missing_name(the_error):
+ # logmessage("extract_missing_name: string was " + str(string))
+ m = nameerror_match.search(str(the_error))
+ if m:
+ return m.group(1)
+ raise the_error
+
+
+def fix_quotes(match):
+ instring = match.group(1)
+ n = len(instring)
+ output = ''
+ i = 0
+ while i < n:
+ if instring[i] == '\u201c' or instring[i] == '\u201d':
+ output += '"'
+ elif instring[i] == '\u2018' or instring[i] == '\u2019':
+ output += "'"
+ elif instring[i] == '&' and i + 4 < n and instring[i:i+5] == '&':
+ output += '&'
+ i += 4
+ else:
+ output += instring[i]
+ i += 1
+ return output
diff --git a/docassemble_base/docassemble/base/hooks.py b/docassemble_base/docassemble/base/hooks.py
new file mode 100644
index 000000000..16f95475c
--- /dev/null
+++ b/docassemble_base/docassemble/base/hooks.py
@@ -0,0 +1,553 @@
+# pylint: disable=unused-argument
+from typing import Any
+from .plugin_manager import pm
+
+
+def get_default_language() -> str:
+ return pm.hook.get_default_language()
+
+def get_default_dialect() -> str:
+ return pm.hook.get_default_dialect()
+
+def get_default_locale() -> str:
+ return pm.hook.get_default_locale()
+
+def get_default_voice() -> str:
+ return pm.hook.get_default_voice()
+
+def get_default_timezone() -> str:
+ return pm.hook.get_default_timezone()
+
+def get_default_country() -> str:
+ return pm.hook.get_default_country()
+
+def get_configuration() -> dict:
+ return pm.hook.get_configuration()
+
+def get_hostname() -> str:
+ return pm.hook.get_hostname()
+
+def get_debug_status() -> bool:
+ return pm.hook.get_debug_status()
+
+def save_numbered_file(filename, orig_path, yaml_file_name=None, uid=None) -> tuple:
+ return pm.hook.save_numbered_file(
+ filename=filename,
+ orig_path=orig_path,
+ yaml_file_name=yaml_file_name,
+ uid=uid,
+ )
+
+def send_mail(the_message, config='default') -> None:
+ return pm.hook.send_mail(the_message=the_message, config=config)
+
+def absolute_filename(the_file) -> Any:
+ return pm.hook.absolute_filename(the_file=the_file)
+
+def write_record(key, data) -> int:
+ return pm.hook.write_record(key=key, data=data)
+
+def read_records(key) -> Any:
+ return pm.hook.read_records(key=key)
+
+def delete_record(key, the_id) -> Any:
+ return pm.hook.delete_record(key=key, the_id=the_id)
+
+def generate_csrf(secret_key=None, token_key=None) -> Any:
+ return pm.hook.generate_csrf(secret_key=secret_key, token_key=token_key)
+
+def url_for(endpoint, **kwargs) -> Any:
+ return pm.hook.url_for(endpoint=endpoint, kwargs=kwargs)
+
+def get_new_file_number(user_code, file_name, yaml_file_name=None) -> Any:
+ return pm.hook.get_new_file_number(
+ user_code=user_code,
+ file_name=file_name,
+ yaml_file_name=yaml_file_name,
+ )
+
+def get_ext_and_mimetype(filename) -> Any:
+ return pm.hook.get_ext_and_mimetype(filename=filename)
+
+def file_finder(file_reference, question=None, folder=None, package=None, filename=None, return_nonexistent=False, uids=None) -> Any:
+ return pm.hook.file_finder(
+ file_reference=file_reference,
+ question=question,
+ folder=folder,
+ package=package,
+ filename=filename,
+ return_nonexistent=return_nonexistent,
+ uids=uids,
+ )
+
+def file_number_finder(file_number, filename=None, uids=None, privileged=False) -> Any:
+ return pm.hook.file_number_finder(
+ file_number=file_number,
+ filename=filename,
+ uids=uids,
+ privileged=privileged,
+ )
+
+def server_sql_get(key, secret=None) -> Any:
+ return pm.hook.server_sql_get(key=key, secret=secret)
+
+def server_sql_defined(key) -> Any:
+ return pm.hook.server_sql_defined(key=key)
+
+def server_sql_set(key, val, encrypted=True, secret=None, the_user_id=None) -> Any:
+ return pm.hook.server_sql_set(
+ key=key,
+ val=val,
+ encrypted=encrypted,
+ secret=secret,
+ the_user_id=the_user_id,
+ )
+
+def server_sql_delete(key) -> Any:
+ return pm.hook.server_sql_delete(key=key)
+
+def server_sql_keys(prefix) -> Any:
+ return pm.hook.server_sql_keys(prefix=prefix)
+
+def alchemy_url(db_config) -> Any:
+ return pm.hook.alchemy_url(db_config=db_config)
+
+def connect_args(db_config) -> Any:
+ return pm.hook.connect_args(db_config=db_config)
+
+def get_default_table_class() -> Any:
+ return pm.hook.get_default_table_class()
+
+def get_default_thead_class() -> Any:
+ return pm.hook.get_default_thead_class()
+
+def to_text(html_doc) -> Any:
+ return pm.hook.to_text(html_doc=html_doc)
+
+def url_finder(file_reference, **kwargs) -> Any:
+ return pm.hook.url_finder(file_reference=file_reference, kwargs=kwargs)
+
+def navigation_bar(nav, interview, wrapper=True, inner_div_class=None, inner_div_extra=None, show_links=None, hide_inactive_subs=True, a_class=None, show_nesting=True, include_arrows=False, always_open=False, return_dict=None) -> Any:
+ return pm.hook.navigation_bar(
+ nav=nav,
+ interview=interview,
+ wrapper=wrapper,
+ inner_div_class=inner_div_class,
+ inner_div_extra=inner_div_extra,
+ show_links=show_links,
+ hide_inactive_subs=hide_inactive_subs,
+ a_class=a_class,
+ show_nesting=show_nesting,
+ include_arrows=include_arrows,
+ always_open=always_open,
+ return_dict=return_dict,
+ )
+
+def chat_partners_available(session_id, yaml_filename, the_user_id, mode, partner_roles) -> Any:
+ return pm.hook.chat_partners_available(
+ session_id=session_id,
+ yaml_filename=yaml_filename,
+ the_user_id=the_user_id,
+ mode=mode,
+ partner_roles=partner_roles,
+ )
+
+def get_chat_log(yaml_filename, session_id, secret, utc=True, timezone=None) -> Any:
+ return pm.hook.get_chat_log(
+ yaml_filename=yaml_filename,
+ session_id=session_id,
+ secret=secret,
+ utc=utc,
+ timezone=timezone,
+ )
+
+def sms_body(phone_number, body='question', config='default') -> Any:
+ return pm.hook.sms_body(phone_number=phone_number, body=body, config=config)
+
+def send_fax(fax_number, the_file, config, country=None) -> Any:
+ return pm.hook.send_fax(
+ fax_number=fax_number,
+ the_file=the_file,
+ config=config,
+ country=country,
+ )
+
+def get_sms_session(phone_number, config='default') -> Any:
+ return pm.hook.get_sms_session(phone_number=phone_number, config=config)
+
+def initiate_sms_session(phone_number, yaml_filename=None, uid=None, secret=None, encrypted=None, user_id=None, email=None, new=False, config='default') -> Any:
+ return pm.hook.initiate_sms_session(
+ phone_number=phone_number,
+ yaml_filename=yaml_filename,
+ uid=uid,
+ secret=secret,
+ encrypted=encrypted,
+ user_id=user_id,
+ email=email,
+ new=new,
+ config=config,
+ )
+
+def terminate_sms_session(phone_number, config='default') -> Any:
+ return pm.hook.terminate_sms_session(phone_number=phone_number, config=config)
+
+def applock(action, application, maxtime=4) -> Any:
+ return pm.hook.applock(action=action, application=application, maxtime=maxtime)
+
+def get_twilio_config() -> Any:
+ return pm.hook.get_twilio_config()
+
+def get_server_redis() -> Any:
+ return pm.hook.get_server_redis()
+
+def get_server_redis_user() -> Any:
+ return pm.hook.get_server_redis_user()
+
+def get_user_object(user_id) -> Any:
+ return pm.hook.get_user_object(user_id=user_id)
+
+def user_id_dict() -> Any:
+ return pm.hook.user_id_dict()
+
+def retrieve_email(email_id) -> Any:
+ return pm.hook.retrieve_email(email_id=email_id)
+
+def retrieve_emails(**kwargs) -> Any:
+ return pm.hook.retrieve_emails(kwargs=kwargs)
+
+def get_short_code(**kwargs) -> Any:
+ return pm.hook.get_short_code(kwargs)
+
+def make_png_for_pdf(doc, prefix, page=None) -> Any:
+ return pm.hook.make_png_for_pdf(doc=doc, prefix=prefix, page=page)
+
+def ocr_google_in_background(image_file, raw_result, user_code) -> Any:
+ return pm.hook.ocr_google_in_background(
+ image_file=image_file,
+ raw_result=raw_result,
+ user_code=user_code,
+ )
+
+def task_ready(task_id) -> Any:
+ return pm.hook.task_ready(task_id=task_id)
+
+def wait_for_task(task_id, timeout=None) -> Any:
+ return pm.hook.wait_for_task(task_id=task_id, timeout=timeout)
+
+def user_interviews(user_id=None, secret=None, exclude_invalid=True, action=None, filename=None, session=None, tag=None, include_dict=True, delete_shared=False, admin=False, start_id=None, temp_user_id=None, query=None, minimal=False) -> Any:
+ return pm.hook.user_interviews(
+ user_id=user_id,
+ secret=secret,
+ exclude_invalid=exclude_invalid,
+ action=action,
+ filename=filename,
+ session=session,
+ tag=tag,
+ include_dict=include_dict,
+ delete_shared=delete_shared,
+ admin=admin,
+ start_id=start_id,
+ temp_user_id=temp_user_id,
+ query=query,
+ minimal=minimal,
+ )
+
+def server_interview_menu(absolute_urls=False, start_new=False, tag=None) -> Any:
+ return pm.hook.server_interview_menu(
+ absolute_urls=absolute_urls,
+ start_new=start_new,
+ tag=tag,
+ )
+
+def server_get_user_list(include_inactive=False, start_id=None) -> Any:
+ return pm.hook.server_get_user_list(
+ include_inactive=include_inactive,
+ start_id=start_id,
+ )
+
+def server_get_user_info(user_id=None, email=None, case_sensitive=False, admin=False) -> Any:
+ return pm.hook.server_get_user_info(
+ user_id=user_id,
+ email=email,
+ case_sensitive=case_sensitive,
+ admin=admin,
+ )
+
+def server_set_user_info(**kwargs) -> Any:
+ return pm.hook.server_set_user_info(kwargs=kwargs)
+
+def make_user_inactive(user_id=None, email=None) -> Any:
+ return pm.hook.make_user_inactive(user_id=user_id, email=email)
+
+def server_get_secret(username, password, case_sensitive=False) -> Any:
+ return pm.hook.server_get_secret(
+ username=username,
+ password=password,
+ case_sensitive=case_sensitive,
+ )
+
+def server_get_session_variables(yaml_filename, session_id, secret=None, simplify=True, use_lock=False) -> Any:
+ return pm.hook.server_get_session_variables(
+ yaml_filename=yaml_filename,
+ session_id=session_id,
+ secret=secret,
+ simplify=simplify,
+ use_lock=use_lock,
+ )
+
+def server_go_back_in_session(yaml_filename, session_id, secret=None, return_question=False, use_lock=False, encode=False) -> Any:
+ return pm.hook.server_go_back_in_session(
+ yaml_filename=yaml_filename,
+ session_id=session_id,
+ secret=secret,
+ return_question=return_question,
+ use_lock=use_lock,
+ encode=encode,
+ )
+
+def server_create_session(yaml_filename, secret, url_args=None, referer=None, req=None) -> Any:
+ return pm.hook.server_create_session(
+ yaml_filename=yaml_filename,
+ secret=secret,
+ url_args=url_args,
+ referer=referer,
+ req=req,
+ )
+
+def server_set_session_variables(yaml_filename, session_id, variables, secret=None, return_question=False, literal_variables=None, del_variables=None, question_name=None, event_list=None, advance_progress_meter=False, post_setting=True, use_lock=False, encode=False, process_objects=False) -> Any:
+ return pm.hook.server_set_session_variables(
+ yaml_filename=yaml_filename,
+ session_id=session_id,
+ variables=variables,
+ secret=secret,
+ return_question=return_question,
+ literal_variables=literal_variables,
+ del_variables=del_variables,
+ question_name=question_name,
+ event_list=event_list,
+ advance_progress_meter=advance_progress_meter,
+ post_setting=post_setting,
+ use_lock=use_lock,
+ encode=encode,
+ process_objects=process_objects,
+ )
+
+def get_privileges_list(admin=False) -> Any:
+ return pm.hook.get_privileges_list(admin=admin)
+
+def add_privilege(privilege) -> Any:
+ return pm.hook.add_privilege(privilege=privilege)
+
+def remove_privilege(privilege) -> Any:
+ return pm.hook.remove_privilege(privilege=privilege)
+
+def add_user_privilege(user_id, privilege) -> Any:
+ return pm.hook.add_user_privilege(user_id=user_id, privilege=privilege)
+
+def remove_user_privilege(user_id, privilege) -> Any:
+ return pm.hook.remove_user_privilege(user_id=user_id, privilege=privilege)
+
+def get_permissions_of_privilege(privilege, privileged=False) -> Any:
+ return pm.hook.get_permissions_of_privilege(privilege=privilege, privileged=privileged)
+
+def server_create_user(email, password, privileges=None, info=None) -> Any:
+ return pm.hook.server_create_user(
+ email=email,
+ password=password,
+ privileges=privileges,
+ info=info,
+ )
+
+def file_set_attributes(file_number, **kwargs) -> Any:
+ return pm.hook.file_set_attributes(
+ file_number=file_number,
+ private=kwargs.get('private', None),
+ persistent=kwargs.get('persistent', None),
+ session=kwargs.get('session', None),
+ filename=kwargs.get('filename', None),
+ )
+
+def file_user_access(file_number, allow_user_id=None, allow_email=None, disallow_user_id=None, disallow_email=None, disallow_all=False) -> Any:
+ return pm.hook.file_user_access(
+ file_number=file_number,
+ allow_user_id=allow_user_id,
+ allow_email=allow_email,
+ disallow_user_id=disallow_user_id,
+ disallow_email=disallow_email,
+ disallow_all=disallow_all,
+ )
+
+def file_privilege_access(file_number, allow=None, disallow=None, disallow_all=False) -> Any:
+ return pm.hook.file_privilege_access(
+ file_number=file_number,
+ allow=allow,
+ disallow=disallow,
+ disallow_all=disallow_all,
+ )
+
+def fg_make_png_for_pdf(doc, prefix, page=None) -> Any:
+ return pm.hook.fg_make_png_for_pdf(doc=doc, prefix=prefix, page=page)
+
+def fg_make_png_for_pdf_path(path, prefix, page=None) -> Any:
+ return pm.hook.fg_make_png_for_pdf_path(path=path, prefix=prefix, page=page)
+
+def fg_make_pdf_for_word_path(path, extension) -> Any:
+ return pm.hook.fg_make_pdf_for_word_path(path=path, extension=extension)
+
+def server_get_question_data(yaml_filename, session_id, secret, use_lock=True, user_dict=None, steps=None, is_encrypted=None, old_user_dict=None, save=True, post_setting=False, advance_progress_meter=False, action=None, encode=False) -> Any:
+ return pm.hook.server_get_question_data(
+ yaml_filename=yaml_filename,
+ session_id=session_id,
+ secret=secret,
+ use_lock=use_lock,
+ user_dict=user_dict,
+ steps=steps,
+ is_encrypted=is_encrypted,
+ old_user_dict=old_user_dict,
+ save=save,
+ post_setting=post_setting,
+ advance_progress_meter=advance_progress_meter,
+ action=action,
+ encode=encode,
+ )
+
+def fix_pickle_obj(data) -> Any:
+ return pm.hook.fix_pickle_obj(data=data)
+
+def get_main_page_parts() -> Any:
+ return pm.hook.get_main_page_parts()
+
+def get_saved_file_class() -> Any:
+ return pm.hook.get_saved_file_class()
+
+def path_from_reference(file_reference) -> Any:
+ return pm.hook.path_from_reference(file_reference=file_reference)
+
+def get_button_class_prefix() -> Any:
+ return pm.hook.get_button_class_prefix()
+
+def write_answer_json(user_code, filename, data, tags=None, persistent=False) -> Any:
+ return pm.hook.write_answer_json(
+ user_code=user_code,
+ filename=filename,
+ data=data,
+ tags=tags,
+ persistent=persistent,
+ )
+
+def read_answer_json(user_code, filename, tags=None, all_tags=False) -> Any:
+ return pm.hook.read_answer_json(
+ user_code=user_code,
+ filename=filename,
+ tags=tags,
+ all_tags=all_tags,
+ )
+
+def delete_answer_json(user_code, filename, tags=None, delete_all=False, delete_persistent=False) -> Any:
+ return pm.hook.delete_answer_json(
+ user_code=user_code,
+ filename=filename,
+ tags=tags,
+ delete_all=delete_all,
+ delete_persistent=delete_persistent,
+ )
+
+def variables_snapshot_connection() -> Any:
+ return pm.hook.variables_snapshot_connection()
+
+def variables_snapshot_connect() -> Any:
+ return pm.hook.variables_snapshot_connect()
+
+def get_referer() -> Any:
+ return pm.hook.get_referer()
+
+def stash_data(data, expire) -> Any:
+ return pm.hook.stash_data(data=data, expire=expire)
+
+def retrieve_stashed_data(key, secret, delete=False, refresh=False) -> Any:
+ return pm.hook.retrieve_stashed_data(key=key, secret=secret, delete=delete, refresh=refresh)
+
+def secure_filename_spaces_ok(filename) -> Any:
+ return pm.hook.secure_filename_spaces_ok(filename=filename)
+
+def secure_filename_unicode_ok(the_filename) -> Any:
+ return pm.hook.secure_filename_unicode_ok(the_filename=the_filename)
+
+def secure_filename(filename) -> Any:
+ return pm.hook.secure_filename(filename=filename)
+
+def transform_json_variables(obj) -> Any:
+ return pm.hook.transform_json_variables(obj=obj)
+
+def get_login_url(**kwargs) -> Any:
+ return pm.hook.get_login_url(kwargs=kwargs)
+
+def server_run_action_in_session(**kwargs) -> Any:
+ return pm.hook.server_run_action_in_session(kwargs=kwargs)
+
+def server_invite_user(email_address, privilege=None, send=True) -> Any:
+ return pm.hook.server_invite_user(
+ email_address=email_address,
+ privilege=privilege,
+ send=send,
+ )
+
+def get_url() -> Any:
+ return pm.hook.get_url()
+
+def release_lock(user_code, filename) -> Any:
+ return pm.hook.release_lock(user_code=user_code, filename=filename)
+
+def register_db(db_name) -> Any:
+ return pm.hook.register_db(db_name=db_name)
+
+def create_objects_in_db(db_name) -> Any:
+ return pm.hook.create_objects_in_db(db_name=db_name)
+
+def get_cloud() -> Any:
+ return pm.hook.get_cloud()
+
+def cloud_custom(provider, config) -> Any:
+ return pm.hook.cloud_custom(provider=provider, config=config)
+
+def google_api() -> Any:
+ return pm.hook.google_api()
+
+def get_mail_class() -> Any:
+ return pm.hook.get_mail_class()
+
+def get_celery_app() -> Any:
+ return pm.hook.get_celery_app()
+
+def get_task(obj) -> Any:
+ return pm.hook.get_task(obj=obj)
+
+def chord(arg) -> Any:
+ return pm.hook.chord(arg=arg)
+
+def fix_ml_files(playground_number, current_project) -> Any:
+ return pm.hook.fix_ml_files(playground_number=playground_number, current_project=current_project)
+
+def write_ml_source(playground, playground_number, current_project, filename, finalize=True):
+ return pm.hook.write_ml_source(playground=playground, playground_number=playground_number, current_project=current_project, filename=filename, finalize=finalize)
+
+def ensure_training_loaded(interview) -> Any:
+ return pm.hook.ensure_training_loaded(interview=interview)
+
+def manage_chat_logs(mode: int, **kwargs) -> None:
+ return pm.hook.manage_chat_logs(mode=mode, kwargs=kwargs)
+
+def manage_global_objects(mode: int, **kwargs) -> None:
+ return pm.hook.manage_global_objects(mode=mode, kwargs=kwargs)
+
+def manage_email_server_objects(mode: int, **kwargs) -> None:
+ return pm.hook.manage_email_server_objects(mode=mode, kwargs=kwargs)
+
+def manage_tts_objects(mode: int, **kwargs) -> None:
+ return pm.hook.manage_tts_objects(mode=mode, kwargs=kwargs)
+
+def get_chat_log_internal(chat_mode, yaml_filename, session_id, user_id, temp_user_id, secret, self_user_id, self_temp_id) -> Any:
+ return pm.hook.get_chat_log(chat_mode=chat_mode, yaml_filename=yaml_filename, session_id=session_id, user_id=user_id, temp_user_id=temp_user_id, secret=secret, self_user_id=self_user_id, self_temp_id=self_temp_id)
+
+def get_ml_info(varname, default_package, default_file) -> Any:
+ return pm.hook.get_ml_info(varname=varname, default_package=default_package, default_file=default_file)
diff --git a/docassemble_base/docassemble/base/hookspecs.py b/docassemble_base/docassemble/base/hookspecs.py
new file mode 100644
index 000000000..2eb834595
--- /dev/null
+++ b/docassemble_base/docassemble/base/hookspecs.py
@@ -0,0 +1,506 @@
+# mypy: disable-error-code="empty-body"
+# pylint: disable=unused-argument
+
+from typing import Any
+import pluggy
+
+hookspec = pluggy.HookspecMarker("docassemble")
+
+@hookspec(firstresult=True)
+def get_default_language() -> str:
+ """Default language"""
+
+@hookspec(firstresult=True)
+def get_default_dialect() -> str:
+ """Default dialect"""
+
+@hookspec(firstresult=True)
+def get_default_locale() -> str:
+ """Default locale"""
+
+@hookspec(firstresult=True)
+def get_default_voice() -> str:
+ """Default voice"""
+
+@hookspec(firstresult=True)
+def get_default_timezone() -> str:
+ """Return the default timezone string for the server.
+
+ Returns the server's local timezone unless a default timezone is configured
+ in the docassemble configuration.
+
+ Returns:
+ str: A timezone string such as ``'America/New_York'``.
+ """
+
+@hookspec(firstresult=True)
+def get_default_country() -> str:
+ """Default country"""
+
+@hookspec(firstresult=True)
+def get_configuration() -> dict:
+ """Get configuration"""
+
+@hookspec(firstresult=True)
+def get_hostname() -> str:
+ """Get hostname"""
+
+@hookspec(firstresult=True)
+def get_debug_status() -> bool:
+ """Get debug status"""
+
+@hookspec(firstresult=True)
+def save_numbered_file(filename, orig_path, yaml_file_name, uid) -> tuple:
+ """Save numbered file"""
+
+@hookspec(firstresult=True)
+def send_mail(the_message, config) -> None:
+ """Send email"""
+
+@hookspec(firstresult=True)
+def absolute_filename(the_file) -> Any:
+ """Get SavedFile or None for a file path"""
+
+@hookspec(firstresult=True)
+def write_record(key, data) -> int:
+ """Store data in the SQL database under the given key.
+
+ Args:
+ key (str): A string key to associate with the record.
+ data: The data to store. Must be pickleable.
+
+ Returns:
+ int: The unique integer ID of the saved record.
+ """
+
+@hookspec(firstresult=True)
+def read_records(key) -> Any:
+ """Return all records stored under the given key.
+
+ Args:
+ key (str): The string key used when calling ``write_record()``.
+
+ Returns:
+ dict: A dictionary mapping unique integer record IDs to the stored data.
+ """
+
+@hookspec(firstresult=True)
+def delete_record(key, the_id) -> Any:
+ """Delete a record from the SQL database by key and ID.
+
+ Args:
+ key (str): The string key associated with the record.
+ the_id (int): The unique integer ID of the record to delete.
+ """
+
+@hookspec(firstresult=True)
+def generate_csrf(secret_key, token_key) -> Any:
+ """Generate CSRF token"""
+
+@hookspec(firstresult=True)
+def url_for(endpoint, kwargs) -> Any:
+ """Wrapper for flask url_for function; kwargs is a dict of keyword arguments"""
+
+@hookspec(firstresult=True)
+def get_new_file_number(user_code, file_name, yaml_file_name) -> Any:
+ """Returns file number for a file"""
+
+@hookspec(firstresult=True)
+def get_ext_and_mimetype(filename) -> Any:
+ """Returns file extension and mimetype"""
+
+@hookspec(firstresult=True)
+def file_finder(file_reference, question, folder, package, filename, return_nonexistent, uids) -> Any:
+ """General-purpose retriever of a file by its reference"""
+
+@hookspec(firstresult=True)
+def file_number_finder(file_number, filename, uids, privileged) -> Any:
+ """Returns information about a file based on its number"""
+
+@hookspec(firstresult=True)
+def server_sql_get(key, secret) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_sql_defined(key) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_sql_set(key, val, encrypted, secret, the_user_id) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_sql_delete(key) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_sql_keys(prefix) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def alchemy_url(db_config) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def connect_args(db_config) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_default_table_class() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_default_thead_class() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def to_text(html_doc) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def url_finder(file_reference, kwargs) -> Any:
+ """Find a URL for a file reference; kwargs is a dict of keyword arguments"""
+
+@hookspec(firstresult=True)
+def navigation_bar(nav, interview, wrapper, inner_div_class, inner_div_extra, show_links, hide_inactive_subs, a_class, show_nesting, include_arrows, always_open, return_dict) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def chat_partners_available(session_id, yaml_filename, the_user_id, mode, partner_roles) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_chat_log(yaml_filename, session_id, secret, utc, timezone) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def sms_body(phone_number, body, config) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def send_fax(fax_number, the_file, config, country) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_sms_session(phone_number, config) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def initiate_sms_session(phone_number, yaml_filename, uid, secret, encrypted, user_id, email, new, config) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def terminate_sms_session(phone_number, config) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def applock(action, application, maxtime) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_twilio_config() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_server_redis() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_server_redis_user() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_user_object(user_id) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def user_id_dict() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def retrieve_email(email_id) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def retrieve_emails(kwargs) -> Any:
+ """Retrieve emails; kwargs is a dict of keyword arguments"""
+
+@hookspec(firstresult=True)
+def get_short_code(kwargs) -> Any:
+ """Get short code; kwargs is a dict of keyword arguments"""
+
+@hookspec(firstresult=True)
+def make_png_for_pdf(doc, prefix, page) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def ocr_google_in_background(image_file, raw_result, user_code) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def task_ready(task_id) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def wait_for_task(task_id, timeout) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def user_interviews(user_id, secret, exclude_invalid, action, filename, session, tag, include_dict, delete_shared, admin, start_id, temp_user_id, query, minimal) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_interview_menu(absolute_urls, start_new, tag) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_get_user_list(include_inactive, start_id) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_get_user_info(user_id, email, case_sensitive, admin) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_set_user_info(kwargs) -> Any:
+ """Set user info; kwargs is a dict of keyword arguments"""
+
+@hookspec(firstresult=True)
+def make_user_inactive(user_id, email) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_get_secret(username, password, case_sensitive) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_get_session_variables(yaml_filename, session_id, secret, simplify, use_lock) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_go_back_in_session(yaml_filename, session_id, secret, return_question, use_lock, encode) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_create_session(yaml_filename, secret, url_args, referer, req) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_set_session_variables(yaml_filename, session_id, variables, secret, return_question, literal_variables, del_variables, question_name, event_list, advance_progress_meter, post_setting, use_lock, encode, process_objects) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_privileges_list(admin) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def add_privilege(privilege) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def remove_privilege(privilege) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def add_user_privilege(user_id, privilege) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def remove_user_privilege(user_id, privilege) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_permissions_of_privilege(privilege, privileged) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_create_user(email, password, privileges, info) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def file_set_attributes(file_number, private, persistent, session, filename) -> Any:
+ """Set attributes on a stored file"""
+
+@hookspec(firstresult=True)
+def file_user_access(file_number, allow_user_id, allow_email, disallow_user_id, disallow_email, disallow_all) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def file_privilege_access(file_number, allow, disallow, disallow_all) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def fg_make_png_for_pdf(doc, prefix, page) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def fg_make_png_for_pdf_path(path, prefix, page) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def fg_make_pdf_for_word_path(path, extension) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def server_get_question_data(yaml_filename, session_id, secret, use_lock, user_dict, steps, is_encrypted, old_user_dict, save, post_setting, advance_progress_meter, action, encode) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def fix_pickle_obj(data) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_main_page_parts() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_saved_file_class() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def path_from_reference(file_reference) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_button_class_prefix() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def write_answer_json(user_code, filename, data, tags, persistent) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def read_answer_json(user_code, filename, tags, all_tags) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def delete_answer_json(user_code, filename, tags, delete_all, delete_persistent) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def variables_snapshot_connection() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def variables_snapshot_connect() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_referer() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def stash_data(data, expire) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def retrieve_stashed_data(key, secret, delete, refresh) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def secure_filename_spaces_ok(filename) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def secure_filename_unicode_ok(the_filename) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def secure_filename(filename) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def transform_json_variables(obj) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_login_url(kwargs) -> Any:
+ """Get login URL; kwargs is a dict of keyword arguments"""
+
+@hookspec(firstresult=True)
+def server_run_action_in_session(kwargs) -> Any:
+ """Run action in session; kwargs is a dict of keyword arguments"""
+
+@hookspec(firstresult=True)
+def server_invite_user(email_address, privilege, send) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_url() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def release_lock(user_code, filename) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def register_db(db_name) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def create_objects_in_db(db_name) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_cloud() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def cloud_custom(provider, config) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def google_api() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_mail_class() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_celery_app() -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_task(obj) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def chord(arg) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def fix_ml_files(playground_number, current_project) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def write_ml_source(playground, playground_number, current_project, filename, finalize) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def ensure_training_loaded(interview) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def manage_chat_logs(mode: int, kwargs: dict) -> None:
+ pass
+
+@hookspec(firstresult=True)
+def manage_global_objects(mode: int, kwargs: dict) -> None:
+ pass
+
+@hookspec(firstresult=True)
+def manage_email_server_objects(mode: int, kwargs: dict) -> None:
+ pass
+
+@hookspec(firstresult=True)
+def manage_tts_objects(mode: int, kwargs: dict) -> None:
+ pass
+
+@hookspec(firstresult=True)
+def get_chat_log_internal(chat_mode, yaml_filename, session_id, user_id, temp_user_id, secret, self_user_id, self_temp_id) -> Any:
+ pass
+
+@hookspec(firstresult=True)
+def get_ml_info(varname, default_package, default_file) -> Any:
+ pass
diff --git a/docassemble_base/docassemble/base/interview_cache.py b/docassemble_base/docassemble/base/interview_cache.py
index b38b5ecc0..0e50bf3d9 100644
--- a/docassemble_base/docassemble/base/interview_cache.py
+++ b/docassemble_base/docassemble/base/interview_cache.py
@@ -1,9 +1,9 @@
from docassemble.base.error import DAException
-import docassemble.base.parse
+from docassemble.base.interview_source import interview_source_from_string
+from docassemble.base.parse import Interview
cache = {}
-
def get_interview(path):
if path is None:
raise DAException("Tried to load interview source with no path")
@@ -11,9 +11,9 @@ def get_interview(path):
the_interview = cache[path]['interview']
the_interview.from_cache = True
else:
- interview_source = docassemble.base.parse.interview_source_from_string(path)
+ interview_source = interview_source_from_string(path)
interview_source.update()
- the_interview = interview_source.get_interview()
+ the_interview = Interview(source=interview_source)
the_interview.from_cache = False
cache[interview_source.path] = {'index': interview_source.get_index(), 'interview': the_interview, 'source': interview_source}
return the_interview
@@ -24,7 +24,7 @@ def clear_cache(path):
del cache[path]
-def cache_valid(questionPath):
- if questionPath in cache and cache[questionPath]['index'] == cache[questionPath]['source'].get_index():
+def cache_valid(question_path):
+ if question_path in cache and cache[question_path]['index'] == cache[question_path]['source'].get_index():
return True
return False
diff --git a/docassemble_base/docassemble/base/interview_source.py b/docassemble_base/docassemble/base/interview_source.py
new file mode 100644
index 000000000..d40ce2e35
--- /dev/null
+++ b/docassemble_base/docassemble/base/interview_source.py
@@ -0,0 +1,311 @@
+# pylint: disable=attribute-defined-outside-init,missing-class-docstring,too-many-instance-attributes
+import re
+import os
+import copy
+import datetime
+import platform
+from jinja2.exceptions import TemplateError
+from jinja2 import FileSystemLoader, select_autoescape, TemplateNotFound
+from jinja2.environment import Environment
+from docassemble.base.error import DANotFoundError, DAError
+from docassemble.base.logger import logmessage
+from docassemble.base.functions import (
+ package_question_filename,
+ standard_question_filename,
+ get_config,
+)
+from docassemble.base.hooks import (
+ absolute_filename,
+ get_configuration,
+ get_server_redis,
+)
+from . import __version__ as da_version
+
+da_arch = platform.machine()
+
+class DAFileSystemLoader(FileSystemLoader):
+
+ def get_source(self, environment, template):
+ if ':' not in template:
+ return super().get_source(environment, template)
+ template_path = None
+ for the_filename in question_path_options(template):
+ if the_filename is not None:
+ template_path = the_filename
+ break
+ if template_path is None or not os.path.isfile(template_path):
+ raise TemplateNotFound(template)
+ fspath = os.fspath(os.path.dirname(template_path))
+ if fspath not in self.searchpath:
+ self.searchpath.append(fspath)
+ mtime = os.path.getmtime(template_path)
+ with open(template_path, 'r', encoding='utf-8') as fp:
+ source = fp.read()
+ return source, template_path, lambda: mtime == os.path.getmtime(template_path)
+
+class InterviewSource:
+
+ def __init__(self, **kwargs):
+ if not hasattr(self, 'package'):
+ self.package = kwargs.get('package', None)
+ self.language = kwargs.get('language', '*')
+ self.dialect = kwargs.get('dialect', None)
+ self.testing = kwargs.get('testing', False)
+ self.translating = kwargs.get('translating', False)
+
+ def __le__(self, other):
+ return str(self) <= (str(other) if isinstance(other, InterviewSource) else other)
+
+ def __ge__(self, other):
+ return str(self) >= (str(other) if isinstance(other, InterviewSource) else other)
+
+ def __gt__(self, other):
+ return str(self) > (str(other) if isinstance(other, InterviewSource) else other)
+
+ def __lt__(self, other):
+ return str(self) < (str(other) if isinstance(other, InterviewSource) else other)
+
+ def __eq__(self, other):
+ return self is other
+
+ def __ne__(self, other):
+ return self is not other
+
+ def __str__(self):
+ if hasattr(self, 'path'):
+ return str(self.path)
+ return 'interviewsource'
+
+ def __hash__(self):
+ if hasattr(self, 'path'):
+ return hash((self.path,))
+ return hash(('interviewsource',))
+
+ def set_path(self, path):
+ self.path = path
+
+ def get_name(self):
+ if ':' in self.path:
+ return self.path
+ return self.get_package() + ':data/questions/' + self.path
+
+ def get_index(self):
+ the_index = get_server_redis().get('da:interviewsource:' + self.path)
+ if the_index is None:
+ # logmessage("Updating index from get_index for " + self.path)
+ the_index = get_server_redis().incr('da:interviewsource:' + self.path)
+ return the_index
+
+ def update_index(self):
+ # logmessage("Updating index for " + self.path)
+ get_server_redis().incr('da:interviewsource:' + self.path)
+
+ def set_filepath(self, filepath):
+ self.filepath = filepath
+
+ def set_directory(self, directory):
+ self.directory = directory
+
+ def set_content(self, content):
+ self.content = content
+
+ def set_language(self, language):
+ self.language = language
+
+ def set_dialect(self, dialect):
+ self.dialect = dialect
+
+ def set_testing(self, testing):
+ self.testing = testing
+
+ def set_package(self, package):
+ self.package = package
+
+ def update(self, **kwargs): # pylint: disable=unused-argument
+ return True
+
+ def get_modtime(self):
+ return self._modtime # pylint: disable=no-member
+
+ def get_language(self):
+ return self.language
+
+ def get_dialect(self):
+ return self.dialect
+
+ def get_package(self):
+ return self.package
+
+ def get_testing(self):
+ return self.testing
+
+ def append(self, path): # pylint: disable=unused-argument
+ return None
+
+
+class InterviewSourceString(InterviewSource):
+
+ def __init__(self, **kwargs):
+ self.set_path(kwargs.get('path', None))
+ self.set_directory(kwargs.get('directory', None))
+ self.set_content(kwargs.get('content', None))
+ self._modtime = datetime.datetime.now(tz=datetime.timezone.utc)
+ super().__init__(**kwargs)
+
+
+class InterviewSourceFile(InterviewSource):
+
+ def __init__(self, **kwargs):
+ self.playground = None
+ if 'filepath' in kwargs:
+ if kwargs['filepath'].__class__.__name__.endswith('SavedFile'):
+ self.playground = kwargs['filepath']
+ if self.playground.subdir and self.playground.subdir != 'default':
+ self.playground_file = os.path.join(self.playground.subdir, self.playground.filename)
+ else:
+ self.playground_file = self.playground.filename
+ # logmessage("The path is " + repr(self.playground.path))
+ if os.path.isfile(self.playground.path) and os.access(self.playground.path, os.R_OK):
+ self.set_filepath(self.playground.path)
+ else:
+ logmessage("Details of playground path reference:")
+ logmessage("Keyword arguments were " + repr(kwargs))
+ for attribute in ['file_number', 'fixed', 'section', 'filename', 'extension', 'directory', 'path', 'modtimes', 'keydict', 'subdir']:
+ if hasattr(self.playground, attribute):
+ logmessage(attribute + " is " + repr(getattr(self.playground, attribute)))
+ else:
+ logmessage(attribute + " did not exist")
+ if os.path.exists(self.playground.path):
+ if os.path.isfile(self.playground.path):
+ if os.access(self.playground.path, os.R_OK):
+ logmessage("path is a file and is readable")
+ else:
+ logmessage("path is a file but is not readable")
+ else:
+ logmessage("path was not a file")
+ else:
+ logmessage("path did not exist")
+ raise DANotFoundError("Reference to invalid playground path.")
+ else:
+ self.set_filepath(kwargs['filepath'])
+ else:
+ self.filepath = None
+ if 'path' in kwargs:
+ self.set_path(kwargs['path'])
+ super().__init__(**kwargs)
+
+ def set_path(self, path):
+ self.path = path
+ parts = path.split(":")
+ if len(parts) == 2:
+ self.package = parts[0]
+ self.basename = parts[1]
+ else:
+ self.package = None
+ # if self.package is None:
+ # m = re.search(r'^/(playground\.[0-9]+)/', path)
+ # if m:
+ # self.package = m.group(1)
+ if self.filepath is None:
+ self.set_filepath(interview_source_from_string(self.path))
+ if self.package is None and re.search(r'docassemble.base.data.', self.filepath):
+ self.package = 'docassemble.base'
+
+ def set_filepath(self, filepath):
+ # logmessage("Called set_filepath with " + str(filepath))
+ self.filepath = filepath
+ if self.filepath is None:
+ self.directory = None
+ else:
+ self.set_directory(os.path.dirname(self.filepath))
+
+ def reset_modtime(self):
+ try:
+ with open(self.filepath, 'a', encoding='utf-8'):
+ os.utime(self.filepath, None)
+ except:
+ logmessage("InterviewSourceFile: could not reset modification time on interview")
+
+ def update(self, **kwargs):
+ try:
+ with open(self.filepath, 'r', encoding='utf-8') as the_file:
+ orig_text = the_file.read()
+ except:
+ return False
+ if not orig_text.startswith('# use jinja'):
+ self.set_content(orig_text)
+ return True
+ env = Environment(
+ loader=DAFileSystemLoader(self.directory),
+ autoescape=select_autoescape()
+ )
+ if kwargs.get('raise_jinja_errors', True):
+ template = env.get_template(os.path.basename(self.filepath))
+ else:
+ try:
+ template = env.get_template(os.path.basename(self.filepath))
+ except TemplateError:
+ self.set_content(orig_text)
+ return True
+ data = copy.deepcopy(get_config('jinja data'))
+ data['__config__'] = copy.deepcopy(get_configuration())
+ data['__version__'] = da_version
+ data['__architecture__'] = da_arch
+ data['__filename__'] = self.path
+ data['__current_package__'] = self.package
+ data['__parent_filename__'] = kwargs.get('parent_source', self).path
+ data['__parent_package__'] = kwargs.get('parent_source', self).package
+ data['__interview_filename__'] = kwargs.get('interview_source', self).path
+ data['__interview_package__'] = kwargs.get('interview_source', self).package
+ data['__hostname__'] = get_config('external hostname', None) or 'localhost'
+ data['__debug__'] = bool(get_config('debug', True))
+ try:
+ self.set_content(template.render(data))
+ except BaseException as err:
+ self.set_content("__error__: " + repr("Jinja2 rendering error: " + err.__class__.__name__ + ": " + str(err)))
+ return True
+
+ def get_modtime(self):
+ # logmessage("get_modtime called in parse where path is " + str(self.path))
+ if self.playground is not None:
+ return self.playground.get_modtime(filename=self.playground_file)
+ self._modtime = os.path.getmtime(self.filepath)
+ return self._modtime
+
+ def append(self, path):
+ new_file = os.path.join(self.directory, path)
+ if os.path.isfile(new_file) and os.access(new_file, os.R_OK):
+ new_source = InterviewSourceFile()
+ new_source.path = path
+ new_source.directory = self.directory
+ new_source.basename = path
+ new_source.filepath = new_file
+ new_source.playground = self.playground
+ if hasattr(self, 'package'):
+ new_source.package = self.package
+ if new_source.update():
+ return new_source
+ return None
+
+def question_path_options(path):
+ n = 0
+ while n < 3:
+ if n == 0:
+ yield package_question_filename(path)
+ elif n == 1:
+ yield standard_question_filename(path)
+ elif n == 2:
+ yield absolute_filename(path)
+ n += 1
+
+def interview_source_from_string(path, **kwargs):
+ if path is None:
+ raise DAError("Passed None to interview_source_from_string")
+ # logmessage("Trying to find " + path)
+ path = re.sub(r'(docassemble.playground[0-9]+[^:]*:)data/questions/(.*)', r'\1\2', path)
+ for the_filename in question_path_options(path):
+ if the_filename is not None:
+ new_source = InterviewSourceFile(filepath=the_filename, path=path)
+ if new_source.update(**kwargs):
+ return new_source
+ raise DANotFoundError("Interview " + str(path) + " not found")
diff --git a/docassemble_base/docassemble/base/jinja.py b/docassemble_base/docassemble/base/jinja.py
new file mode 100644
index 000000000..43afa012c
--- /dev/null
+++ b/docassemble_base/docassemble/base/jinja.py
@@ -0,0 +1,570 @@
+import re
+import os
+from collections import abc, namedtuple
+from itertools import groupby, chain
+from docxtpl import RichText, DocxTemplate
+from jinja2 import ChainableUndefined
+from jinja2 import meta as jinja2meta
+from jinja2.environment import Environment
+from jinja2.ext import Extension
+from jinja2.lexer import Token
+from jinja2.runtime import StrictUndefined, UndefinedError
+from jinja2.utils import internalcode, missing, object_type_repr
+from .error import DAError, DASourceError, DAAttributeError, DAIndexError
+from .filter.docx import inline_markdown_to_docx, markdown_to_docx
+from .filter.utils import sanitize_xml
+from .functions import (
+ redact,
+ phone_number_in_e164,
+ manual_line_breaks,
+ bold,
+ single_to_double_newlines,
+ single_paragraph,
+ alpha,
+ qr_code,
+ country_name,
+ phone_number_formatted,
+ verbatim,
+ roman,
+ italic,
+)
+from .helpers import extract_missing_name, fix_quotes
+from .language.capitalization import capitalize
+from .language.currency import currency
+from .language.language import (
+ salutation,
+ add_separators,
+ comma_and_list,
+ title_case,
+ comma_list,
+)
+from .language.numbers import ordinal_number, nice_number, ordinal
+from .language.utils import fix_punctuation
+from .language.words import word
+from .thread_context import this_thread
+from .dates import (
+ month_of,
+ format_date,
+ day_of,
+ format_time,
+ format_datetime,
+ year_of,
+ dow_of,
+)
+
+NoneType = type(None)
+
+class DAExtension(Extension):
+
+ def parse(self, parser):
+ raise NotImplementedError()
+
+ def filter_stream(self, stream):
+ # in_var = False
+ met_pipe = False
+ for token in stream:
+ if token.type == 'variable_begin':
+ # in_var = True
+ met_pipe = False
+ if token.type == 'variable_end':
+ # in_var = False
+ if not met_pipe:
+ yield Token(token.lineno, 'pipe', None)
+ yield Token(token.lineno, 'name', 'ampersand_filter')
+ # if in_var and token.type == 'pipe':
+ # met_pipe = True
+ yield token
+
+
+def ampersand_filter(value):
+ if value.__class__.__name__ in ('DAFile', 'DALink', 'DAStaticFile', 'DAFileCollection', 'DAFileList'):
+ return value
+ if value.__class__.__name__ in ('CustomInlineImage', 'InlineImage', 'RichText', 'Listing', 'Document', 'Subdoc', 'DALazyTemplate', 'Markup'):
+ return str(value)
+ if isinstance(value, (int, bool, float, NoneType)):
+ return value
+ if not isinstance(value, str):
+ value = str(value)
+ value = sanitize_xml(value)
+ if '' in value or ' ' in value:
+ return re.sub(r'&(?!#?[0-9A-Za-z]+;)', '&', value)
+ for auto_filter in this_thread.misc.get('auto jinja filter', []):
+ value = auto_filter(value)
+ return re.sub(r'>', '>', re.sub(r'<', '<', re.sub(r'&(?!#?[0-9A-Za-z]+;)', '&', value)))
+
+
+class DAStrictUndefined(StrictUndefined):
+ __slots__ = ('_undefined_type',)
+
+ def __init__(self, hint=None, obj=missing, name=None, exc=UndefinedError, accesstype=None): # pylint: disable=super-init-not-called
+ self._undefined_hint = hint
+ self._undefined_obj = obj
+ self._undefined_name = name
+ self._undefined_exception = exc
+ self._undefined_type = accesstype
+
+ @internalcode
+ def __getattr__(self, name):
+ if name[:2] == '__':
+ raise AttributeError(name)
+ return self._fail_with_undefined_error(attribute=True)
+
+ @internalcode
+ def __getitem__(self, index):
+ if index[:2] == '__':
+ raise IndexError(index)
+ return self._fail_with_undefined_error(item=True)
+
+ @internalcode
+ def _fail_with_undefined_error(self, *args, **kwargs):
+ if self._undefined_obj is missing:
+ hint = "'%s' is undefined" % self._undefined_name
+ elif self._undefined_type == 'item' and hasattr(self._undefined_obj, 'instanceName'):
+ hint = "'%s[%r]' is undefined" % (
+ self._undefined_obj.instanceName,
+ self._undefined_name
+ )
+ elif 'attribute' in kwargs or self._undefined_type == 'attribute':
+ if hasattr(self._undefined_obj, 'instanceName'):
+ hint = "'%s.%s' is undefined" % (
+ self._undefined_obj.instanceName,
+ self._undefined_name
+ )
+ else:
+ hint = '%r has no attribute %r' % (
+ object_type_repr(self._undefined_obj),
+ self._undefined_name
+ )
+ else:
+ if hasattr(self._undefined_obj, 'instanceName'):
+ hint = "'%s[%r]' is undefined" % (
+ self._undefined_obj.instanceName,
+ self._undefined_name
+ )
+ else:
+ hint = '%s has no element %r' % (
+ object_type_repr(self._undefined_obj),
+ self._undefined_name
+ )
+ raise self._undefined_exception(hint)
+ __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
+ __truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \
+ __mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \
+ __lt__ = __le__ = __gt__ = __ge__ = __int__ = \
+ __float__ = __complex__ = __pow__ = __rpow__ = __sub__ = \
+ __rsub__ = __iter__ = __str__ = __len__ = __nonzero__ = __eq__ = \
+ __ne__ = __bool__ = __hash__ = _fail_with_undefined_error
+
+
+class DASkipUndefined(ChainableUndefined):
+ """Undefined handler for Jinja2 exceptions that allows rendering most
+ templates that have undefined variables. It will not fix all broken
+ templates. For example, if the missing variable is used in a complex
+ mathematical expression it may still break (but expressions with only two
+ elements should render as '').
+ """
+
+ def __init__(self, *pargs, **kwargs): # pylint: disable=super-init-not-called
+ # Handle the way Docassemble DAEnvironment triggers attribute errors
+ pass
+
+ def __str__(self) -> str:
+ return ''
+
+ def __call__(self, *pargs, **kwargs) -> "DASkipUndefined":
+ return self
+
+ __getitem__ = __getattr__ = __call__
+
+ def __eq__(self, *pargs) -> bool:
+ return False
+
+ # need to return a bool type
+ __bool__ = __ne__ = __le__ = __lt__ = __gt__ = __ge__ = __nonzero__ = __eq__
+
+ # let undefined variables work in for loops
+
+ def __iter__(self, *pargs) -> "DASkipUndefined":
+ return self
+
+ def __next__(self, *pargs) -> None:
+ raise StopIteration
+
+ # need to return an int type
+
+ def __int__(self, *pargs) -> int:
+ return 0
+
+ __len__ = __int__
+
+ # need to return a float type
+
+ def __float__(self, *pargs) -> float:
+ return 0.0
+
+ # need to return complex type
+
+ def __complex__(self, *pargs) -> complex:
+ return 0j
+
+ def __add__(self, *pargs, **kwargs) -> str:
+ return self.__str__()
+
+ # type can be anything. we want it to work with `str()` function though
+ # and we do not want to silently give wrong math results.
+ # note that this means 1 + (undefined) or (undefined) + 1 will work but not 1 + (undefined) + 1
+ __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
+ __truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \
+ __mod__ = __rmod__ = __pos__ = __neg__ = __pow__ = __rpow__ = \
+ __sub__ = __rsub__ = __hash__ = __add__
+
+
+class DAEnvironment(Environment):
+
+ def from_string(self, source, **kwargs): # pylint: disable=arguments-differ
+ source = re.sub(r'({[\%\{].*?[\%\}]})', fix_quotes, source)
+ return super().from_string(source, **kwargs)
+
+ def getitem(self, obj, argument):
+ try:
+ return obj[argument]
+ except (DAAttributeError, DAIndexError) as err:
+ varname = extract_missing_name(err)
+ if 'pending_error' in this_thread.misc:
+ del this_thread.misc['pending_error']
+ return self.undefined(obj=missing, name=varname)
+ except (AttributeError, TypeError, LookupError):
+ if 'pending_error' in this_thread.misc:
+ del this_thread.misc['pending_error']
+ return self.undefined(obj=obj, name=argument, accesstype='item')
+
+ def getattr(self, obj, attribute):
+ try:
+ return getattr(obj, attribute)
+ except DAAttributeError as err:
+ if 'pending_error' in this_thread.misc:
+ del this_thread.misc['pending_error']
+ varname = extract_missing_name(err)
+ return self.undefined(obj=missing, name=varname)
+ except AttributeError:
+ if 'pending_error' in this_thread.misc:
+ del this_thread.misc['pending_error']
+ return self.undefined(obj=obj, name=attribute, accesstype='attribute')
+
+
+def mygetattr(y, attr, default=None):
+ for attribute in attr.split('.'):
+ y = getattr(y, attribute, default)
+ return y
+
+
+def str_or_original(y, case_sensitive):
+ if case_sensitive:
+ if hasattr(y, 'instanceName'):
+ if y.__class__.__name__ in ('Value', 'PeriodicValue'):
+ return y.amount()
+ return str(y)
+ return y
+ if hasattr(y, 'instanceName'):
+ if y.__class__.__name__ in ('Value', 'PeriodicValue'):
+ return y.amount()
+ return str(y).lower()
+ try:
+ return y.lower()
+ except:
+ return y
+
+
+def dictsort_filter(dictionary, case_sensitive=False, by='key', reverse=False):
+ if by == 'value':
+ return sorted(dictionary.items(), key=lambda y: str_or_original(y[1], case_sensitive), reverse=reverse)
+ return sorted(dictionary.items(), key=lambda y: str_or_original(y[0], case_sensitive), reverse=reverse)
+
+
+def sort_filter(the_array, reverse=False, case_sensitive=False, attribute=None):
+ if attribute is None:
+ if not case_sensitive:
+ def key_func(y):
+ return str_or_original(y, case_sensitive)
+ else:
+ key_func = None
+ else:
+ if isinstance(attribute, list):
+ attributes = [str(y).strip() for y in attribute]
+ else:
+ attributes = [y.strip() for y in str(attribute).split(',')]
+ def key_func(y):
+ return [str_or_original(mygetattr(y, attribute), case_sensitive) for attribute in attributes]
+ return sorted(the_array, key=key_func, reverse=reverse)
+
+_GroupTuple = namedtuple('_GroupTuple', ['grouper', 'list'])
+_GroupTuple.__repr__ = tuple.__repr__
+_GroupTuple.__str__ = tuple.__str__
+
+
+def groupby_filter(the_array, attr_name):
+
+ def func(y):
+ return mygetattr(y, attr_name)
+ return [_GroupTuple(key, list(values)) for key, values in groupby(sorted(the_array, key=func), func)]
+
+
+def max_filter(the_array, case_sensitive=False, attribute=None):
+ it = iter(the_array)
+ try:
+ first = next(it)
+ except StopIteration:
+ raise DAError("max: list was empty")
+ if attribute:
+ def key_func(y):
+ return str_or_original(mygetattr(y, attribute), case_sensitive=case_sensitive)
+ else:
+ def key_func(y):
+ return str_or_original(y, case_sensitive=case_sensitive)
+ return max(chain([first], it), key=key_func)
+
+
+def min_filter(the_array, case_sensitive=False, attribute=None):
+ it = iter(the_array)
+ try:
+ first = next(it)
+ except StopIteration:
+ raise DAError("min: list was empty")
+ if attribute:
+ def key_func(y):
+ return str_or_original(mygetattr(y, attribute), case_sensitive=case_sensitive)
+ else:
+ def key_func(y):
+ return str_or_original(y, case_sensitive=case_sensitive)
+ return min(chain([first], it), key=key_func)
+
+
+def sum_filter(the_array, attribute=None, start=0):
+ if attribute is not None:
+ the_array = [mygetattr(y, attribute) for y in the_array]
+ return sum(the_array, start)
+
+
+def unique_filter(the_array, case_sensitive=False, attribute=None):
+ seen = set()
+ if attribute is None:
+ for item in the_array:
+ new_item = str_or_original(item, case_sensitive)
+ if new_item not in seen:
+ seen.add(new_item)
+ yield item
+ else:
+ for item in the_array:
+ new_item = str_or_original(mygetattr(item, attribute), case_sensitive)
+ if new_item not in seen:
+ seen.add(new_item)
+ yield mygetattr(item, attribute)
+
+
+def join_filter(the_array, d="", attribute=None):
+ if attribute is not None:
+ return d.join([str(mygetattr(y, attribute)) for y in the_array])
+ return d.join([str(y) for y in the_array])
+
+
+def attr_filter(var, attr_name):
+ return mygetattr(var, attr_name)
+
+
+def selectattr_filter(*pargs, **kwargs):
+ if len(pargs) > 2:
+ the_array = pargs[0]
+ attr_name = pargs[1]
+ func_name = pargs[2]
+ env = custom_jinja_env()
+ def func(item):
+ return env.call_test(func_name, item, pargs[3:], kwargs)
+ for item in the_array:
+ if func(mygetattr(item, attr_name)):
+ yield item
+ else:
+ for item in pargs[0]:
+ if mygetattr(item, pargs[1]):
+ yield item
+
+
+def rejectattr_filter(*pargs, **kwargs):
+ if len(pargs) > 2:
+ the_array = pargs[0]
+ attr_name = pargs[1]
+ func_name = pargs[2]
+ env = custom_jinja_env()
+ def func(item):
+ return env.call_test(func_name, item, pargs[3:], kwargs)
+ for item in the_array:
+ if not func(mygetattr(item, attr_name)):
+ yield item
+ else:
+ for item in pargs[0]:
+ if not mygetattr(item, pargs[1]):
+ yield item
+
+
+def chain_filter(*pargs, **kwargs): # pylint: disable=unused-argument
+ the_list = []
+ for parg in pargs:
+ if isinstance(parg, str):
+ the_list.append(parg)
+ elif (hasattr(parg, 'instanceName') and hasattr(parg, 'elements')):
+ if isinstance(parg.elements, dict):
+ for sub_parg in parg.values():
+ the_list.append(sub_parg)
+ else:
+ for sub_parg in parg:
+ the_list.append(sub_parg)
+ elif isinstance(parg, abc.Iterable):
+ for sub_parg in parg:
+ the_list.append(sub_parg)
+ else:
+ the_list.append(parg)
+ return chain(*the_list)
+
+
+def map_filter(*pargs, **kwargs):
+ if len(pargs) >= 2:
+ the_array = pargs[0]
+ the_filter = pargs[1]
+ env = custom_jinja_env()
+ if the_filter not in env.filters:
+ raise DAError('filter passed to map() does not exist')
+ for item in the_array:
+ yield env.call_filter(the_filter, item, pargs[2:], kwargs)
+ else:
+ if 'attribute' in kwargs:
+ if 'default' in kwargs:
+ for item in pargs[0]:
+ yield mygetattr(item, kwargs['attribute'], kwargs['default'])
+ else:
+ for item in pargs[0]:
+ yield mygetattr(item, kwargs['attribute'])
+ elif 'index' in kwargs:
+ if 'default' in kwargs:
+ for item in pargs[0]:
+ yield item.get(kwargs['index'], kwargs['default'])
+ else:
+ for item in pargs[0]:
+ yield item[kwargs['index']]
+ elif 'function' in kwargs:
+ the_kwargs = kwargs.get('kwargs', {})
+ the_pargs = kwargs.get('pargs', [])
+ if not isinstance(the_kwargs, dict):
+ raise DAError('kwargs passed to map() must be a dictionary')
+ if not isinstance(the_pargs, list):
+ raise DAError('pargs passed to map() must be a list')
+ for item in pargs[0]:
+ yield kwargs['function'](item, *the_pargs, **the_kwargs)
+ else:
+ raise DAError('map() must refer to a function, index, attribute, or filter')
+
+
+def markdown_filter(text):
+ return markdown_to_docx(text, this_thread.current_question, this_thread.misc.get('docx_template', None))
+
+
+def inline_markdown_filter(text):
+ return inline_markdown_to_docx(text, this_thread.current_question, this_thread.misc.get('docx_template', None))
+
+
+def get_builtin_jinja_filters():
+ return {
+ 'ampersand_filter': ampersand_filter,
+ 'markdown': markdown_filter,
+ 'add_separators': add_separators,
+ 'inline_markdown': inline_markdown_filter,
+ 'paragraphs': single_to_double_newlines,
+ 'manual_line_breaks': manual_line_breaks,
+ 'RichText': RichText,
+ 'groupby': groupby_filter,
+ 'max': max_filter,
+ 'min': min_filter,
+ 'sum': sum_filter,
+ 'unique': unique_filter,
+ 'join': join_filter,
+ 'attr': attr_filter,
+ 'selectattr': selectattr_filter,
+ 'rejectattr': rejectattr_filter,
+ 'sort': sort_filter,
+ 'dictsort': dictsort_filter,
+ 'format_date': format_date,
+ 'format_datetime': format_datetime,
+ 'format_time': format_time,
+ 'month_of': month_of,
+ 'year_of': year_of,
+ 'day_of': day_of,
+ 'dow_of': dow_of,
+ 'qr_code': qr_code,
+ 'nice_number': nice_number,
+ 'ordinal': ordinal,
+ 'ordinal_number': ordinal_number,
+ 'currency': currency,
+ 'comma_list': comma_list,
+ 'comma_and_list': comma_and_list,
+ 'capitalize': capitalize,
+ 'salutation': salutation,
+ 'alpha': alpha,
+ 'roman': roman,
+ 'word': word,
+ 'bold': bold,
+ 'italic': italic,
+ 'title_case': title_case,
+ 'single_paragraph': single_paragraph,
+ 'phone_number_formatted': phone_number_formatted,
+ 'phone_number_in_e164': phone_number_in_e164,
+ 'country_name': country_name,
+ 'fix_punctuation': fix_punctuation,
+ 'redact': redact,
+ 'verbatim': verbatim,
+ 'map': map_filter,
+ 'chain': chain_filter,
+ 'any': any,
+ 'all': all
+ }
+
+
+registered_jinja_filters = {}
+
+
+def custom_jinja_env(skip_undefined=False):
+ if skip_undefined:
+ env = DAEnvironment(undefined=DASkipUndefined, extensions=[DAExtension])
+ else:
+ env = DAEnvironment(undefined=DAStrictUndefined, extensions=[DAExtension])
+ env.filters.update(registered_jinja_filters)
+ env.filters.update(get_builtin_jinja_filters())
+ return env
+
+
+def register_jinja_filter(filter_name, func):
+ if filter_name in get_builtin_jinja_filters():
+ raise DAError("Cannot register filter with same name as built-in filter %s" % filter_name)
+ registered_jinja_filters[filter_name] = func
+
+
+def get_docx_variables(the_path):
+ names = set()
+ if not os.path.isfile(the_path):
+ raise DASourceError("Missing docx template file " + os.path.basename(the_path))
+ try:
+ docx_template = DocxTemplate(the_path)
+ docx_template.render_init()
+ the_env = custom_jinja_env()
+ the_xml = docx_template.get_xml()
+ the_xml = re.sub(r'])', r'\n 1:
+ return a[0].upper() + a[1:]
+ return a
+
+capitalize = language_function_constructor('capitalize')
+
+update_language_function('*', 'capitalize', capitalize_default)
diff --git a/docassemble_base/docassemble/base/language/control.py b/docassemble_base/docassemble/base/language/control.py
new file mode 100644
index 000000000..5d73f5673
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/control.py
@@ -0,0 +1,146 @@
+import locale
+from docassemble.base.thread_context import this_thread
+from docassemble.base.logger import logmessage
+
+def get_language():
+ """Return the current language code.
+
+ Returns:
+ str: The current language code (e.g., ``'en'``, ``'es'``).
+ """
+ return this_thread.language
+
+
+def set_language(lang, dialect=None, voice=None):
+ """Set the language used for linguistic functions and the web application.
+
+ Does not change the Python locale; call ``update_locale()`` for that.
+ Should be called in an ``initial`` code block so it takes effect on every
+ page load.
+
+ Args:
+ lang (str): A lowercase ISO-639-1 or ISO-639-3 language code
+ (e.g., ``'en'``, ``'es'``, ``'fr'``).
+ dialect (str, optional): A dialect code for the text-to-speech engine.
+ Defaults to None.
+ voice (str, optional): A voice name for the text-to-speech engine.
+ Defaults to None.
+ """
+ try:
+ if dialect:
+ this_thread.dialect = dialect
+ elif lang != this_thread.language:
+ this_thread.dialect = None
+ except:
+ pass
+ try:
+ if voice:
+ this_thread.voice = voice
+ elif lang != this_thread.language:
+ this_thread.voice = None
+ except:
+ pass
+ this_thread.language = lang
+
+
+def set_country(country):
+ """Set the current country used for phone number formatting and other locale features.
+
+ Args:
+ country (str): A two-letter uppercase ISO 3166-1 alpha-2 country code
+ (e.g., ``'US'``, ``'GB'``, ``'DE'``).
+ """
+ this_thread.country = country
+
+
+def get_country():
+ """Return the current country code.
+
+ Returns:
+ str: A two-letter uppercase ISO 3166-1 alpha-2 country code
+ (e.g., ``'US'``). Defaults to ``'US'`` unless configured otherwise.
+ """
+ return this_thread.country
+
+
+def get_dialect():
+ """Return the current dialect.
+
+ Returns:
+ str: The dialect code set by the ``dialect`` keyword argument to
+ :func:`set_language`, or ``None`` if no dialect has been set.
+ """
+ return this_thread.dialect
+
+
+def get_voice():
+ """Return the current voice.
+
+ Returns:
+ str: The voice name set by the ``voice`` keyword argument to
+ :func:`set_language`, or ``None`` if no voice has been set.
+ """
+ return this_thread.voice
+
+
+def set_locale(*pargs, **kwargs):
+ """Set the current locale string and/or locale convention overrides.
+
+ Calling ``set_locale('FR.utf8')`` stores the locale string so that
+ :func:`get_locale` returns it. The actual Python locale does not change
+ until :func:`update_locale` is called. Keyword arguments such as
+ ``currency_symbol`` override individual locale conventions used by
+ functions like :func:`currency` and :func:`currency_symbol`.
+
+ Args:
+ *pargs: An optional locale string (e.g. ``'FR.utf8'``).
+ **kwargs: Locale convention overrides (e.g. ``currency_symbol='€'``).
+ """
+ if len(pargs) == 1:
+ this_thread.locale = pargs[0]
+ if len(kwargs):
+ this_thread.misc['locale_overrides'] = kwargs
+
+
+def get_locale(*pargs):
+ """Return the current locale setting or a specific locale convention.
+
+ With no arguments, returns the locale string previously set with
+ :func:`set_locale`. With one argument, returns the value of the named
+ locale convention (e.g. ``'currency_symbol'``), taking into account any
+ overrides set with :func:`set_locale`.
+
+ Args:
+ *pargs: An optional locale convention name (e.g.
+ ``'currency_symbol'``).
+
+ Returns:
+ str or None: The locale string when called with no arguments, or the
+ value of the requested locale convention (``None`` if not found).
+ """
+ if len(pargs) == 1:
+ if 'locale_overrides' in this_thread.misc and pargs[0] in this_thread.misc['locale_overrides']:
+ return this_thread.misc['locale_overrides'][pargs[0]]
+ return locale.localeconv().get(pargs[0], None)
+ return this_thread.locale
+
+
+def update_locale():
+ """Update the Python locale based on the current language and locale settings.
+
+ Applies the locale string previously set with :func:`set_locale` (combined
+ with the current language from :func:`get_language` when necessary) so that
+ Python's ``locale`` module reflects the desired locale. This is required
+ for functions like :func:`currency` and :func:`currency_symbol` to produce
+ locale-appropriate formatting.
+ """
+ if '_' in this_thread.locale:
+ the_locale = str(this_thread.locale)
+ else:
+ the_locale = str(this_thread.language) + '_' + str(this_thread.locale)
+ try:
+ locale.setlocale(locale.LC_ALL, the_locale)
+ except BaseException as err:
+ logmessage("update_locale error: unable to set the locale to " + the_locale)
+ logmessage(err.__class__.__name__ + ": " + str(err))
+ locale.setlocale(locale.LC_ALL, 'en_US.utf8')
diff --git a/docassemble_base/docassemble/base/language/core.py b/docassemble_base/docassemble/base/language/core.py
new file mode 100644
index 000000000..fb767caf2
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/core.py
@@ -0,0 +1,38 @@
+from typing import Callable
+from jinja2.runtime import Undefined
+from docassemble.base.logger import logmessage
+from docassemble.base.thread_context import this_thread
+
+language_functions: dict[str, dict[str, Callable]] = {}
+
+def update_language_function(lang, term, func):
+ if term not in language_functions:
+ language_functions[term] = {}
+ language_functions[term][lang] = func
+
+
+def ensure_definition(*pargs, **kwargs):
+ for val in pargs:
+ if isinstance(val, Undefined):
+ str(val)
+ for val in kwargs.values():
+ if isinstance(val, Undefined):
+ str(val)
+
+
+def language_function_constructor(term):
+
+ def func(*args, **kwargs):
+ ensure_definition(*args, **kwargs)
+ language = kwargs.get('language', None)
+ if language is None:
+ language = this_thread.language
+ if language in language_functions[term]:
+ return language_functions[term][language](*args, **kwargs)
+ if '*' in language_functions[term]:
+ return language_functions[term]['*'](*args, **kwargs)
+ if 'en' in language_functions[term]:
+ logmessage("Term " + str(term) + " is not defined for language " + str(language))
+ return language_functions[term]['en'](*args, **kwargs)
+ raise SystemError("term " + str(term) + " not defined in language_functions for English or *")
+ return func
diff --git a/docassemble_base/docassemble/base/language/currency.py b/docassemble_base/docassemble/base/language/currency.py
new file mode 100644
index 000000000..44a08671d
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/currency.py
@@ -0,0 +1,125 @@
+import locale
+from docassemble.base.hooks import get_configuration
+from docassemble.base.thread_context import this_thread
+from .control import get_locale
+from .core import (
+ language_function_constructor,
+ ensure_definition,
+ update_language_function,
+ language_functions,
+)
+
+currency = language_function_constructor('currency')
+currency_symbol = language_function_constructor('currency_symbol')
+
+if currency.__doc__ is None:
+ currency.__doc__ = """Format a number as a currency value using the current locale.
+
+ Args:
+ value: The numeric value to format.
+ **kwargs: Optional keyword arguments including ``decimals`` (bool,
+ default ``True``), ``symbol`` (str override for the currency
+ symbol), and ``symbol_precedes`` (bool controlling symbol
+ position).
+
+ Returns:
+ str: The formatted currency string (e.g. ``currency(45.2)`` returns
+ ``'$45.20'`` for a US locale).
+ """
+if currency_symbol.__doc__ is None:
+ currency_symbol.__doc__ = """Return the currency symbol for the current locale.
+
+ Returns:
+ str: The currency symbol (e.g. ``'$'`` for a US locale). Respects
+ overrides set via :func:`set_locale` or the ``currency symbol``
+ configuration setting.
+ """
+
+def currency_symbol_default(**kwargs): # pylint: disable=unused-argument
+ """Returns the currency symbol for the current locale."""
+ return str(locale.localeconv()['currency_symbol'])
+
+
+def currency_default(the_value, **kwargs):
+ """Returns the value as a currency, according to the conventions of
+ the current locale. Use the optional keyword argument
+ decimals=False if you do not want to see decimal places in the
+ number, and the optional currency_symbol for a different symbol
+ than the default.
+
+ """
+ decimals = kwargs.get('decimals', True)
+ symbol = kwargs.get('symbol', None)
+ symbol_precedes = kwargs.get('symbol_precedes', None)
+ ensure_definition(the_value, decimals, symbol)
+ obj_type = type(the_value).__name__
+ if obj_type in ['FinancialList', 'PeriodicFinancialList']:
+ the_value = the_value.total()
+ elif obj_type in ['Value', 'PeriodicValue']:
+ if the_value.exists:
+ the_value = the_value.amount()
+ else:
+ the_value = 0
+ elif obj_type == 'DACatchAll':
+ the_value = float(the_value)
+ try:
+ float(the_value)
+ except:
+ return ''
+ the_float_value = float(the_value)
+ the_symbol = None
+ if symbol is not None:
+ the_symbol = symbol
+ elif 'locale_overrides' in this_thread.misc and 'currency_symbol' in this_thread.misc['locale_overrides']:
+ the_symbol = this_thread.misc['locale_overrides']['currency_symbol']
+ elif language_functions['currency_symbol']['*'] is not currency_symbol_default:
+ the_symbol = currency_symbol()
+ the_symbol_precedes = None
+ if symbol_precedes is not None:
+ the_symbol_precedes = symbol_precedes
+ elif 'locale_overrides' in this_thread.misc and the_float_value < 0 and 'n_cs_precedes' in this_thread.misc['locale_overrides']:
+ the_symbol_precedes = bool(this_thread.misc['locale_overrides']['n_cs_precedes'])
+ elif 'locale_overrides' in this_thread.misc and 'p_cs_precedes' in this_thread.misc['locale_overrides']:
+ the_symbol_precedes = bool(this_thread.misc['locale_overrides']['p_cs_precedes'])
+ if the_symbol is None and the_symbol_precedes is None and decimals:
+ return str(locale.currency(the_float_value, symbol=True, grouping=True))
+ if the_symbol is None:
+ the_symbol = currency_symbol()
+ if the_symbol_precedes is None:
+ if the_float_value < 0:
+ the_symbol_precedes = bool(get_locale('n_cs_precedes'))
+ else:
+ the_symbol_precedes = bool(get_locale('p_cs_precedes'))
+ output = ''
+ if the_symbol_precedes:
+ output += the_symbol
+ if the_float_value < 0:
+ if get_locale('n_sep_by_space'):
+ output += ' '
+ elif get_locale('p_sep_by_space'):
+ output += ' '
+ if decimals:
+ output += locale.format_string('%.' + str(get_configuration().get('currency decimal places', locale.localeconv()['frac_digits'])) + 'f', the_float_value, grouping=True, monetary=True)
+ else:
+ output += locale.format_string("%d", int(the_float_value), grouping=True, monetary=True)
+ if not the_symbol_precedes:
+ if the_float_value < 0:
+ if get_locale('n_sep_by_space'):
+ output += ' '
+ elif get_locale('p_sep_by_space'):
+ output += ' '
+ output += the_symbol
+ return output
+
+
+def get_currency_symbol():
+ """Returns the current setting for the currency symbol if there is
+ one, and otherwise returns the default currency symbol.
+
+ """
+ if 'locale_overrides' in this_thread.misc and 'currency_symbol' in this_thread.misc['locale_overrides']:
+ return this_thread.misc['locale_overrides']['currency_symbol']
+ return currency_symbol()
+
+
+update_language_function('*', 'currency_symbol', currency_symbol_default)
diff --git a/docassemble_base/docassemble/base/language/language.py b/docassemble_base/docassemble/base/language/language.py
new file mode 100644
index 000000000..466d3ba55
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/language.py
@@ -0,0 +1,747 @@
+import titlecase
+from .core import ensure_definition, language_function_constructor, language_functions
+from .capitalization import capitalize
+from .words import word
+from .numbers import nice_number, nice_number_default
+from .language_en import (
+ verb_present_en,
+ verb_past_en,
+ possessify_en,
+ indefinite_article_en,
+ noun_singular_en,
+ noun_plural_en,
+ add_separators_en,
+ comma_list_en,
+ comma_and_list_en,
+)
+from .language_es import (
+ verb_past_es,
+ noun_plural_es,
+ indefinite_article_es,
+ noun_singular_es,
+ verb_present_es,
+ comma_and_list_es,
+)
+from .language_de import (
+ indefinite_article_de,
+ noun_singular_de,
+ verb_past_de,
+ verb_present_de,
+ noun_plural_de,
+ comma_and_list_de,
+)
+from .language_fr import verb_past_fr, noun_plural_fr, noun_singular_fr, verb_present_fr
+from .language_it import (
+ noun_singular_it,
+ noun_plural_it,
+ verb_past_it,
+ indefinite_article_it,
+ verb_present_it,
+)
+from .language_nl import verb_past_nl, verb_present_nl, noun_singular_nl, noun_plural_nl
+from .currency import currency_default
+from .capitalization import capitalize_default
+
+def titlecasestr(text):
+ return titlecase.titlecase(str(text))
+
+
+def salutation_default(indiv, **kwargs):
+ """Returns Mr., Ms., etc. for an individual."""
+ with_name = kwargs.get('with_name', False)
+ with_name_and_punctuation = kwargs.get('with_name_and_punctuation', False)
+ ensure_definition(indiv, with_name, with_name_and_punctuation)
+ used_gender = False
+ if hasattr(indiv, 'salutation_to_use') and indiv.salutation_to_use is not None:
+ salut = indiv.salutation_to_use
+ elif hasattr(indiv, 'is_doctor') and indiv.is_doctor:
+ salut = 'Dr.'
+ elif hasattr(indiv, 'is_judge') and indiv.is_judge:
+ salut = 'Judge'
+ elif hasattr(indiv, 'name') and hasattr(indiv.name, 'suffix') and indiv.name.suffix in ('MD', 'PhD'):
+ salut = 'Dr.'
+ elif hasattr(indiv, 'name') and hasattr(indiv.name, 'suffix') and indiv.name.suffix == 'J':
+ salut = 'Judge'
+ elif indiv.gender == 'female':
+ used_gender = True
+ salut = 'Ms.'
+ else:
+ used_gender = True
+ salut = 'Mr.'
+ if with_name_and_punctuation or with_name:
+ if used_gender and indiv.gender not in ('male', 'female'):
+ salut_and_name = indiv.name.full()
+ else:
+ salut_and_name = salut + ' ' + indiv.name.last
+ if with_name_and_punctuation:
+ if hasattr(indiv, 'is_friendly') and indiv.is_friendly:
+ punct = ','
+ else:
+ punct = ':'
+ return salut_and_name + punct
+ if with_name:
+ return salut_and_name
+ return salut
+
+
+def quantity_noun_default(the_number, noun, **kwargs):
+ as_integer = kwargs.get('as_integer', True)
+ capitalize_arg = kwargs.get('capitalize', False)
+ language = kwargs.get('language', None)
+ ensure_definition(the_number, noun, as_integer, capitalize_arg, language)
+ if as_integer:
+ the_number = int(round(the_number))
+ result = nice_number(the_number, language=language) + " " + noun_plural(noun, the_number, language=language)
+ if capitalize_arg:
+ return capitalize(result)
+ return result
+
+
+def prefix_constructor(prefix):
+
+ def func(the_word, **kwargs):
+ ensure_definition(the_word, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(str(prefix)) + str(the_word)
+ return str(prefix) + str(the_word)
+ return func
+
+
+def double_prefix_constructor_reverse(prefix_one, prefix_two):
+
+ def func(word_one, word_two, **kwargs):
+ ensure_definition(word_one, word_two, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(str(prefix_one)) + str(word_two) + str(prefix_two) + str(word_one)
+ return str(prefix_one) + str(word_two) + str(prefix_two) + str(word_one)
+ return func
+
+
+def prefix_constructor_two_arguments(prefix, **kwargs): # pylint: disable=unused-argument
+
+ def func(word_one, word_two, **kwargs):
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(str(prefix)) + str(word_one) + ' ' + str(word_two)
+ return str(prefix) + str(word_one) + ' ' + str(word_two)
+ return func
+
+
+def middle_constructor(middle, **kwargs): # pylint: disable=unused-argument
+
+ def func(a, b, **kwargs):
+ ensure_definition(a, b, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(str(a)) + str(middle) + str(b)
+ return str(a) + str(middle) + str(b)
+ return func
+
+
+def a_preposition_b_default(a, b, **kwargs):
+ ensure_definition(a, b, **kwargs)
+ if hasattr(a, 'preposition'):
+ preposition = word(a.preposition)
+ else:
+ preposition = word('in the')
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(str(a)) + str(' ' + preposition + ' ') + str(b)
+ return str(a) + str(' ' + preposition + ' ') + str(b)
+
+
+
+in_the = language_function_constructor('in_the')
+a_preposition_b = language_function_constructor('a_preposition_b')
+a_in_the_b = language_function_constructor('a_in_the_b')
+i_subjective = language_function_constructor('i_subjective')
+he_subjective = language_function_constructor('he_subjective')
+she_subjective = language_function_constructor('she_subjective')
+genderless_subjective = language_function_constructor('genderless_subjective')
+myself = language_function_constructor('myself')
+itself = language_function_constructor('itself')
+herself = language_function_constructor('herself')
+himself = language_function_constructor('himself')
+themselves = language_function_constructor('themselves')
+genderless_self = language_function_constructor('genderless_self')
+yourself = language_function_constructor('yourself')
+yourselves = language_function_constructor('yourselves')
+ourselves = language_function_constructor('ourselves')
+you_subjective = language_function_constructor('you_subjective')
+you_subjective_plural = language_function_constructor('you_subjective_plural')
+we_subjective = language_function_constructor('we_subjective')
+they_subjective = language_function_constructor('they_subjective')
+it_subjective = language_function_constructor('it_subjective')
+it_objective = language_function_constructor('it_objective')
+them_objective = language_function_constructor('them_objective')
+genderless_objective = language_function_constructor('genderless_objective')
+me_objective = language_function_constructor('me_objective')
+him_objective = language_function_constructor('him_objective')
+her_objective = language_function_constructor('her_objective')
+our_objective = language_function_constructor('our_objective')
+you_objective = language_function_constructor('you_objective')
+you_objective_plural = language_function_constructor('you_objective_plural')
+us_objective = language_function_constructor('us_objective')
+are_we = language_function_constructor('are_we')
+are_you = language_function_constructor('are_you')
+are_you_plural = language_function_constructor('are_you_plural')
+am_i = language_function_constructor('am_i')
+her = language_function_constructor('her')
+his = language_function_constructor('his')
+are_word = language_function_constructor('are_word')
+is_word = language_function_constructor('is_word')
+their = language_function_constructor('their')
+my_possessive = language_function_constructor('my_possessive')
+our_possessive = language_function_constructor('our_possessive')
+of_the = language_function_constructor('of_the')
+your = language_function_constructor('your')
+your_plural = language_function_constructor('your_plural')
+some = language_function_constructor('some')
+its = language_function_constructor('its')
+the = language_function_constructor('the')
+these = language_function_constructor('these')
+this = language_function_constructor('this')
+does_a_b = language_function_constructor('does_a_b')
+do_a_b = language_function_constructor('do_a_b')
+did_a_b = language_function_constructor('did_a_b')
+did_a_b_plural = language_function_constructor('did_a_b_plural')
+do_i = language_function_constructor('do_i')
+do_we = language_function_constructor('do_we')
+do_you = language_function_constructor('do_you')
+do_you_plural = language_function_constructor('do_you_plural')
+did_i = language_function_constructor('did_i')
+did_we = language_function_constructor('did_we')
+did_you = language_function_constructor('did_you')
+did_you_plural = language_function_constructor('did_you_plural')
+was_i = language_function_constructor('was_i')
+were_we = language_function_constructor('were_we')
+were_you = language_function_constructor('were_you')
+were_you_plural = language_function_constructor('were_you_plural')
+was_a_b = language_function_constructor('was_a_b')
+were_a_b = language_function_constructor('were_a_b')
+were_a_b_plural = language_function_constructor('were_a_b_plural')
+have_i = language_function_constructor('have_i')
+have_we = language_function_constructor('have_we')
+have_you = language_function_constructor('have_you')
+have_you_plural = language_function_constructor('have_you_plural')
+has_a_b = language_function_constructor('has_a_b')
+have_a_b = language_function_constructor('have_a_b')
+verb_past = language_function_constructor('verb_past')
+verb_present = language_function_constructor('verb_present')
+noun_plural = language_function_constructor('noun_plural')
+noun_singular = language_function_constructor('noun_singular')
+indefinite_article = language_function_constructor('indefinite_article')
+period_list = language_function_constructor('period_list')
+name_suffix = language_function_constructor('name_suffix')
+possessify = language_function_constructor('possessify')
+possessify_long = language_function_constructor('possessify_long')
+comma_list = language_function_constructor('comma_list')
+comma_and_list = language_function_constructor('comma_and_list')
+add_separators = language_function_constructor('add_separators')
+quantity_noun = language_function_constructor('quantity_noun')
+title_case = language_function_constructor('title_case')
+salutation = language_function_constructor('salutation')
+
+if verb_past.__doc__ is None:
+ verb_past.__doc__ = """Return the past tense of a verb.
+
+ Args:
+ verb (str): The verb to conjugate.
+ **kwargs: Optional conjugation parameters passed to the underlying
+ language function (e.g. ``'3gp'`` for third-person past tense).
+
+ Returns:
+ str: The past-tense form of the verb (e.g. ``verb_past('help')``
+ returns ``'helped'``).
+ """
+if verb_present.__doc__ is None:
+ verb_present.__doc__ = """Return the present tense of a verb.
+
+ Args:
+ verb (str): The verb to conjugate (may be in any tense).
+ **kwargs: Optional conjugation parameters passed to the underlying
+ language function (e.g. ``'3sg'`` for third-person singular).
+
+ Returns:
+ str: The present-tense form of the verb (e.g.
+ ``verb_present('helped', '3sg')`` returns ``'helps'``).
+ """
+if noun_plural.__doc__ is None:
+ noun_plural.__doc__ = """Return the plural form of a noun.
+
+ Args:
+ noun (str): The noun to pluralize.
+ *pargs: An optional quantity (number, list, dict, or set). When the
+ quantity is exactly ``1`` the singular form is returned instead.
+ **kwargs: Pass ``noun_is_singular=True`` to skip singularization
+ before pluralizing.
+
+ Returns:
+ str: The plural form of the noun, or the singular form if the
+ optional quantity equals ``1``.
+ """
+if noun_singular.__doc__ is None:
+ noun_singular.__doc__ = """Return the singular form of a noun.
+
+ Args:
+ noun (str): The noun to singularize.
+ *pargs: An optional quantity (number, list, dict, or set). When the
+ quantity is not ``1`` the original noun is returned unchanged.
+
+ Returns:
+ str: The singular form of the noun, or the original noun when the
+ optional quantity is not ``1``.
+ """
+if indefinite_article.__doc__ is None:
+ indefinite_article.__doc__ = """Return a noun preceded by the appropriate indefinite article.
+
+ Args:
+ noun (str): The noun phrase to precede with an article.
+ **kwargs: Additional keyword arguments passed to the underlying
+ language function.
+
+ Returns:
+ str: The noun prefixed with ``'a'`` or ``'an'`` as appropriate
+ (e.g. ``indefinite_article('apple')`` returns ``'an apple'``).
+ """
+if capitalize.__doc__ is None:
+ capitalize.__doc__ = """Return the input string with the first letter capitalized.
+
+ Args:
+ a (str): The string to capitalize.
+ **kwargs: Additional keyword arguments passed to the underlying
+ language function.
+
+ Returns:
+ str: The input string with its first character converted to
+ upper case.
+ """
+if period_list.__doc__ is None:
+ period_list.__doc__ = """Return a list of per-year period options for use in multiple-choice fields.
+
+ Returns:
+ list: A list of ``[number, label]`` pairs representing common
+ payment periods (e.g. ``[[12, 'Per Month'], [1, 'Per Year'],
+ [52, 'Per Week'], ...]``).
+ """
+if name_suffix.__doc__ is None:
+ name_suffix.__doc__ = """Return a list of common name suffixes for use in multiple-choice fields.
+
+ Returns:
+ list: A list of name suffix strings such as
+ ``['Jr', 'Sr', 'II', 'III', 'IV', 'V', 'VI']``.
+ """
+if possessify.__doc__ is None:
+ possessify.__doc__ = """Return the possessive phrase combining two arguments.
+
+ Args:
+ a: The possessor.
+ b: The thing possessed.
+ **kwargs: Additional keyword arguments passed to the underlying
+ language function.
+
+ Returns:
+ str: A possessive phrase such as ``"a's b"``.
+ """
+if possessify_long.__doc__ is None:
+ possessify_long.__doc__ = """Return the long possessive phrase combining two arguments.
+
+ Args:
+ a: The possessor.
+ b: The thing possessed.
+ **kwargs: Additional keyword arguments passed to the underlying
+ language function.
+
+ Returns:
+ str: A possessive phrase of the form ``"the b of a"``.
+ """
+if comma_list.__doc__ is None:
+ comma_list.__doc__ = """Return the items joined by commas.
+
+ Args:
+ *pargs: Items to join, or a single iterable as the first argument.
+ **kwargs: Optional ``comma_string`` (default ``', '``) to customize
+ the separator.
+
+ Returns:
+ str: The items separated by commas (e.g.
+ ``comma_list('lions', 'tigers', 'bears')`` returns
+ ``'lions, tigers, bears'``).
+ """
+if comma_and_list.__doc__ is None:
+ comma_and_list.__doc__ = """Return the items joined by commas with "and" before the last item.
+
+ Args:
+ *pargs: Items to join, or a single iterable as the first argument.
+ **kwargs: Optional keyword arguments including ``oxford`` (bool,
+ default ``True``), ``and_string`` (default ``'and'``),
+ ``comma_string``, ``before_and``, and ``after_and``.
+
+ Returns:
+ str: An English-language listing such as ``'lions, tigers, and
+ bears'``.
+ """
+if add_separators.__doc__ is None:
+ add_separators.__doc__ = """Return the list items as strings with separators appended.
+
+ Appends ``;`` to all items except the penultimate, which gets
+ ``'; and'``, and the last, which gets ``'.'``.
+
+ Args:
+ the_list: The list of items to process.
+ separator (str, optional): Separator appended to middle items.
+ Defaults to ``';'``.
+ last_separator (str, optional): Separator appended to the
+ penultimate item. Defaults to ``'; and'``.
+ end_mark (str, optional): Mark appended to the final item.
+ Defaults to ``'.'``.
+
+ Returns:
+ list: A list of strings with separators appended.
+ """
+if nice_number.__doc__ is None:
+ nice_number.__doc__ = """Return a number expressed as a word for small values, or as a formatted numeral.
+
+ Args:
+ num: The number to convert.
+ **kwargs: Optional keyword arguments including ``capitalize``
+ (bool), ``language`` (str), and ``use_word`` (bool, default
+ ``False``).
+
+ Returns:
+ str: The number as a word (e.g. ``nice_number(4)`` returns
+ ``'four'``) or as a locale-formatted numeral for larger values.
+ """
+if quantity_noun.__doc__ is None:
+ quantity_noun.__doc__ = """Return a number combined with a noun in the appropriate singular or plural form.
+
+ Combines :func:`nice_number` and :func:`noun_plural`. Rounds the number
+ to the nearest integer unless ``as_integer=False`` is passed.
+
+ Args:
+ num: The quantity.
+ noun (str): The singular noun.
+ **kwargs: Optional keyword arguments including ``as_integer``
+ (bool, default ``True``) and other arguments accepted by
+ :func:`nice_number`.
+
+ Returns:
+ str: The quantity and noun combined (e.g. ``quantity_noun(2,
+ 'apple')`` returns ``'two apples'``).
+ """
+if title_case.__doc__ is None:
+ title_case.__doc__ = """Return the input string with the first letter of each word capitalized.
+
+ Args:
+ a (str): The string to convert to title case.
+ **kwargs: Additional keyword arguments passed to the underlying
+ language function.
+
+ Returns:
+ str: The title-cased string (e.g. ``title_case('the importance of
+ being ernest')`` returns ``'The Importance of Being Ernest'``).
+ """
+
+language_functions.update({
+ 'in_the': {
+ 'en': prefix_constructor('in the ')
+ },
+ 'a_preposition_b': {
+ 'en': a_preposition_b_default
+ },
+ 'a_in_the_b': {
+ 'en': middle_constructor(' in the ')
+ },
+ 'i_subjective': {
+ 'en': lambda *pargs, **kwargs: word('I', **kwargs)
+ },
+ 'he_subjective': {
+ 'en': lambda *pargs, **kwargs: word('he', **kwargs)
+ },
+ 'she_subjective': {
+ 'en': lambda *pargs, **kwargs: word('she', **kwargs)
+ },
+ 'genderless_subjective': {
+ 'en': lambda *pargs, **kwargs: word('they', **kwargs)
+ },
+ 'myself': {
+ 'en': lambda *pargs, **kwargs: word('myself', **kwargs)
+ },
+ 'itself': {
+ 'en': lambda *pargs, **kwargs: word('itself', **kwargs)
+ },
+ 'herself': {
+ 'en': lambda *pargs, **kwargs: word('herself', **kwargs)
+ },
+ 'himself': {
+ 'en': lambda *pargs, **kwargs: word('himself', **kwargs)
+ },
+ 'themselves': {
+ 'en': lambda *pargs, **kwargs: word('themselves', **kwargs)
+ },
+ 'genderless_self': {
+ 'en': lambda *pargs, **kwargs: word('themself', **kwargs)
+ },
+ 'yourself': {
+ 'en': lambda *pargs, **kwargs: word('yourself', **kwargs)
+ },
+ 'yourselves': {
+ 'en': lambda *pargs, **kwargs: word('yourselves', **kwargs)
+ },
+ 'ourselves': {
+ 'en': lambda *pargs, **kwargs: word('ourselves', **kwargs)
+ },
+ 'you_subjective': {
+ 'en': lambda *pargs, **kwargs: word('you', **kwargs)
+ },
+ 'you_subjective_plural': {
+ 'en': lambda *pargs, **kwargs: word('you', **kwargs)
+ },
+ 'we_subjective': {
+ 'en': lambda *pargs, **kwargs: word('we', **kwargs)
+ },
+ 'they_subjective': {
+ 'en': lambda *pargs, **kwargs: word('they', **kwargs)
+ },
+ 'it_subjective': {
+ 'en': lambda *pargs, **kwargs: word('it', **kwargs)
+ },
+ 'it_objective': {
+ 'en': lambda *pargs, **kwargs: word('it', **kwargs)
+ },
+ 'them_objective': {
+ 'en': lambda *pargs, **kwargs: word('them', **kwargs)
+ },
+ 'genderless_objective': {
+ 'en': lambda *pargs, **kwargs: word('them', **kwargs)
+ },
+ 'me_objective': {
+ 'en': lambda *pargs, **kwargs: word('me', **kwargs)
+ },
+ 'him_objective': {
+ 'en': lambda *pargs, **kwargs: word('him', **kwargs)
+ },
+ 'her_objective': {
+ 'en': lambda *pargs, **kwargs: word('her', **kwargs)
+ },
+ 'our_objective': {
+ 'en': lambda *pargs, **kwargs: word('our', **kwargs)
+ },
+ 'you_objective': {
+ 'en': lambda *pargs, **kwargs: word('you', **kwargs)
+ },
+ 'you_objective_plural': {
+ 'en': lambda *pargs, **kwargs: word('you', **kwargs)
+ },
+ 'us_objective': {
+ 'en': lambda *pargs, **kwargs: word('us', **kwargs)
+ },
+ 'are_we': {
+ 'en': lambda *pargs, **kwargs: word('are we', **kwargs)
+ },
+ 'are_you': {
+ 'en': lambda *pargs, **kwargs: word('are you', **kwargs)
+ },
+ 'are_you_plural': {
+ 'en': lambda *pargs, **kwargs: word('are you', **kwargs)
+ },
+ 'am_i': {
+ 'en': lambda *pargs, **kwargs: word('am I', **kwargs)
+ },
+ 'her': {
+ 'en': prefix_constructor('her ')
+ },
+ 'his': {
+ 'en': prefix_constructor('his ')
+ },
+ 'are_word': {
+ 'en': prefix_constructor('are ')
+ },
+ 'is_word': {
+ 'en': prefix_constructor('is ')
+ },
+ 'their': {
+ 'en': prefix_constructor('their ')
+ },
+ 'my_possessive': {
+ 'en': prefix_constructor('my ')
+ },
+ 'our_possessive': {
+ 'en': prefix_constructor('our ')
+ },
+ 'of_the': {
+ 'en': prefix_constructor('of the ')
+ },
+ 'your': {
+ 'en': prefix_constructor('your ')
+ },
+ 'your_plural': {
+ 'en': prefix_constructor('your ')
+ },
+ 'some': {
+ 'en': prefix_constructor('some ')
+ },
+ 'its': {
+ 'en': prefix_constructor('its ')
+ },
+ 'the': {
+ 'en': prefix_constructor('the ')
+ },
+ 'these': {
+ 'en': prefix_constructor('these ')
+ },
+ 'this': {
+ 'en': prefix_constructor('this ')
+ },
+ 'does_a_b': {
+ 'en': prefix_constructor_two_arguments('does ')
+ },
+ 'do_a_b': {
+ 'en': prefix_constructor_two_arguments('do ')
+ },
+ 'did_a_b': {
+ 'en': prefix_constructor_two_arguments('did ')
+ },
+ 'did_a_b_plural': {
+ 'en': prefix_constructor_two_arguments('did ')
+ },
+ 'do_i': {
+ 'en': prefix_constructor('do I ')
+ },
+ 'do_we': {
+ 'en': prefix_constructor('do we ')
+ },
+ 'do_you': {
+ 'en': prefix_constructor('do you ')
+ },
+ 'do_you_plural': {
+ 'en': prefix_constructor('do you ')
+ },
+ 'did_i': {
+ 'en': prefix_constructor('did I ')
+ },
+ 'did_we': {
+ 'en': prefix_constructor('did we ')
+ },
+ 'did_you': {
+ 'en': prefix_constructor('did you ')
+ },
+ 'did_you_plural': {
+ 'en': prefix_constructor('did you ')
+ },
+ 'was_i': {
+ 'en': prefix_constructor('was I ')
+ },
+ 'were_we': {
+ 'en': prefix_constructor('were we ')
+ },
+ 'were_you': {
+ 'en': prefix_constructor('were you ')
+ },
+ 'were_you_plural': {
+ 'en': prefix_constructor('were you ')
+ },
+ 'was_a_b': {
+ 'en': prefix_constructor_two_arguments('was ')
+ },
+ 'were_a_b': {
+ 'en': prefix_constructor_two_arguments('were ')
+ },
+ 'were_a_b_plural': {
+ 'en': prefix_constructor_two_arguments('were ')
+ },
+ 'have_i': {
+ 'en': prefix_constructor('have I ')
+ },
+ 'have_we': {
+ 'en': prefix_constructor('have we ')
+ },
+ 'have_you': {
+ 'en': prefix_constructor('have you ')
+ },
+ 'have_you_plural': {
+ 'en': prefix_constructor('have you ')
+ },
+ 'has_a_b': {
+ 'en': prefix_constructor_two_arguments('has ')
+ },
+ 'have_a_b': {
+ 'en': prefix_constructor_two_arguments('have ')
+ },
+ 'verb_past': {
+ 'en': verb_past_en,
+ 'es': verb_past_es,
+ 'de': verb_past_de,
+ 'fr': verb_past_fr,
+ 'it': verb_past_it,
+ 'nl': verb_past_nl
+ },
+ 'verb_present': {
+ 'en': verb_present_en,
+ 'es': verb_present_es,
+ 'de': verb_present_de,
+ 'fr': verb_present_fr,
+ 'it': verb_present_it,
+ 'nl': verb_present_nl
+ },
+ 'noun_plural': {
+ 'en': noun_plural_en,
+ 'es': noun_plural_es,
+ 'de': noun_plural_de,
+ 'fr': noun_plural_fr,
+ 'it': noun_plural_it,
+ 'nl': noun_plural_nl
+ },
+ 'noun_singular': {
+ 'en': noun_singular_en,
+ 'es': noun_singular_es,
+ 'de': noun_singular_de,
+ 'fr': noun_singular_fr,
+ 'it': noun_singular_it,
+ 'nl': noun_singular_nl
+ },
+ 'indefinite_article': {
+ 'en': indefinite_article_en,
+ 'es': indefinite_article_es,
+ 'de': indefinite_article_de,
+ 'it': indefinite_article_it
+ },
+ 'period_list': {
+ '*': lambda: [[12, word("Per Month")], [1, word("Per Year")], [52, word("Per Week")], [24, word("Twice Per Month")], [26, word("Every Two Weeks")]]
+ },
+ 'name_suffix': {
+ '*': lambda: ['Jr', 'Sr', 'II', 'III', 'IV', 'V', 'VI']
+ },
+ 'currency': {
+ '*': currency_default
+ },
+ 'possessify': {
+ 'en': possessify_en
+ },
+ 'possessify_long': {
+ 'en': double_prefix_constructor_reverse('the ', ' of the ')
+ },
+ 'comma_and_list': {
+ 'en': comma_and_list_en,
+ 'es': comma_and_list_es,
+ 'de': comma_and_list_de
+ },
+ 'comma_list': {
+ 'en': comma_list_en
+ },
+ 'add_separators': {
+ 'en': add_separators_en
+ },
+ 'nice_number': {
+ '*': nice_number_default
+ },
+ 'quantity_noun': {
+ '*': quantity_noun_default
+ },
+ 'capitalize': {
+ '*': capitalize_default
+ },
+ 'title_case': {
+ '*': titlecasestr
+ },
+ 'salutation': {
+ '*': salutation_default
+ }
+})
diff --git a/docassemble_base/docassemble/base/language/language_de.py b/docassemble_base/docassemble/base/language/language_de.py
new file mode 100644
index 000000000..d7b41be2a
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/language_de.py
@@ -0,0 +1,74 @@
+from docassemble.base.pattern import pattern_de
+from .core import ensure_definition
+from .capitalization import capitalize
+from .numbers import number_or_length
+from .language_en import comma_and_list_en
+
+def comma_and_list_de(*pargs, **kwargs):
+ if 'and_string' not in kwargs:
+ kwargs['and_string'] = 'und'
+ if 'oxford' not in kwargs:
+ kwargs['oxford'] = False
+ return comma_and_list_en(*pargs, **kwargs)
+
+
+def verb_present_de(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(str(arg))
+ if len(new_args) < 2:
+ new_args.append('3sg')
+ if new_args[1] == 'pl':
+ new_args[1] = '3pl'
+ output = pattern_de.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def verb_past_de(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(arg)
+ if len(new_args) < 2:
+ new_args.append('3sgp')
+ if new_args[1] == 'ppl':
+ new_args[1] = '3ppl'
+ output = pattern_de.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_plural_de(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if kwargs.get('noun_is_singular', False):
+ noun = pargs[0]
+ else:
+ noun = noun_singular_de(pargs[0])
+ if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
+ return str(noun)
+ output = pattern_de.pluralize(str(noun))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_singular_de(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
+ return pargs[0]
+ output = pattern_de.singularize(str(pargs[0]))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def indefinite_article_de(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ output = pattern_de.article(str(pargs[0]).lower()) + " " + str(pargs[0])
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
diff --git a/docassemble_base/docassemble/base/language/language_en.py b/docassemble_base/docassemble/base/language/language_en.py
new file mode 100644
index 000000000..f766d0bf3
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/language_en.py
@@ -0,0 +1,162 @@
+from collections.abc import Iterable
+from docassemble.base.pattern import pattern_en
+from docassemble.base.thread_context import this_thread
+from docassemble.base.language.utils import fix_punctuation
+from .core import ensure_definition
+from .capitalization import capitalize
+from .numbers import number_or_length
+from .words import word
+
+def noun_plural_en(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if kwargs.get('noun_is_singular', False):
+ noun = pargs[0]
+ else:
+ noun = noun_singular_en(pargs[0])
+ if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
+ return str(noun)
+ output = pattern_en.pluralize(str(noun))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_singular_en(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
+ return pargs[0]
+ output = pattern_en.singularize(str(pargs[0]))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def indefinite_article_en(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ output = pattern_en.article(str(pargs[0]).lower()) + " " + str(pargs[0])
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+def verb_present_en(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(str(arg))
+ if len(new_args) < 2:
+ new_args.append('3sg')
+ output = pattern_en.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def verb_past_en(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(arg)
+ if len(new_args) < 2:
+ new_args.append('3sgp')
+ output = pattern_en.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def possessify_en(a, b, **kwargs):
+ ensure_definition(a, b, **kwargs)
+ if this_thread.evaluation_context == 'docx':
+ apostrophe = "’"
+ else:
+ apostrophe = "'"
+ if 'plural' in kwargs and kwargs['plural']:
+ middle = apostrophe + " "
+ else:
+ middle = apostrophe + "s "
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(str(a)) + str(middle) + str(b)
+ return str(a) + str(middle) + str(b)
+
+
+def comma_list_en(*pargs, **kwargs):
+ """Returns the arguments separated by commas. If the first argument is a list,
+ that list is used. Otherwise, the arguments are treated as individual items.
+ See also comma_and_list()."""
+ ensure_definition(*pargs, **kwargs)
+ comma_string = kwargs.get('comma_string', ', ')
+ the_list = []
+ for parg in pargs:
+ if isinstance(parg, str):
+ the_list.append(parg)
+ elif (hasattr(parg, 'instanceName') and hasattr(parg, 'elements')) or isinstance(parg, Iterable):
+ for sub_parg in parg:
+ the_list.append(str(sub_parg))
+ else:
+ the_list.append(str(parg))
+ return comma_string.join(the_list)
+
+
+def comma_and_list_en(*pargs, **kwargs):
+ """Returns an English-language listing of the arguments. If the first argument is a list,
+ that list is used. Otherwise, the arguments are treated as individual items in the list.
+ Use the optional argument oxford=False if you do not want a comma before the "and."
+ See also comma_list()."""
+ ensure_definition(*pargs, **kwargs)
+ and_string = kwargs.get('and_string', word('and'))
+ comma_string = kwargs.get('comma_string', ', ')
+ if 'oxford' in kwargs and kwargs['oxford'] is False:
+ extracomma = ""
+ else:
+ extracomma = comma_string.strip()
+ before_and = kwargs.get('before_and', ' ')
+ after_and = kwargs.get('after_and', ' ')
+ the_list = []
+ for parg in pargs:
+ if isinstance(parg, str):
+ the_list.append(parg)
+ elif (hasattr(parg, 'instanceName') and hasattr(parg, 'elements')) or isinstance(parg, Iterable):
+ for sub_parg in parg:
+ the_list.append(str(sub_parg))
+ else:
+ the_list.append(str(parg))
+ if len(the_list) == 0:
+ return str('')
+ if len(the_list) == 1:
+ return the_list[0]
+ if len(the_list) == 2:
+ return the_list[0] + before_and + and_string + after_and + the_list[1]
+ return comma_string.join(the_list[:-1]) + extracomma + before_and + and_string + after_and + the_list[-1]
+
+
+def add_separators_en(*pargs, **kwargs):
+ """Accepts a list and returns a list, with semicolons after each item,
+ except "and" after the penultimate item and a period after the
+ last.
+
+ """
+ ensure_definition(*pargs, **kwargs)
+ separator = kwargs.get('separator', ';')
+ last_separator = kwargs.get('last_separator', '; ' + word("and"))
+ end_mark = kwargs.get('end_mark', '.')
+ the_list = []
+ for parg in pargs:
+ if isinstance(parg, str):
+ the_list.append(parg.rstrip())
+ elif (hasattr(parg, 'instanceName') and hasattr(parg, 'elements')) or isinstance(parg, Iterable):
+ for sub_parg in parg:
+ the_list.append(str(sub_parg).rstrip())
+ else:
+ the_list.append(str(parg).rstrip())
+ if len(the_list) == 0:
+ return the_list
+ if len(the_list) == 1:
+ return [fix_punctuation(the_list[0], mark=end_mark)]
+ for indexno in range(len(the_list) - 2): # for 4: 0, 1; for 3: 0; for 2: []
+ the_list[indexno] = the_list[indexno].rstrip(',')
+ the_list[indexno] = fix_punctuation(the_list[indexno], mark=separator)
+ if not the_list[-2].endswith(last_separator):
+ the_list[-2] = the_list[-2].rstrip(last_separator[0])
+ the_list[-2] += last_separator
+ the_list[-1] = fix_punctuation(the_list[-1], mark=end_mark)
+ return the_list
diff --git a/docassemble_base/docassemble/base/language/language_es.py b/docassemble_base/docassemble/base/language/language_es.py
new file mode 100644
index 000000000..30d4fb873
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/language_es.py
@@ -0,0 +1,72 @@
+from docassemble.base.pattern import pattern_es
+from .core import ensure_definition
+from .capitalization import capitalize
+from .numbers import number_or_length
+from .language_en import comma_and_list_en
+
+def comma_and_list_es(*pargs, **kwargs):
+ if 'and_string' not in kwargs:
+ kwargs['and_string'] = 'y'
+ return comma_and_list_en(*pargs, **kwargs)
+
+
+def verb_present_es(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(str(arg))
+ if len(new_args) < 2:
+ new_args.append('3sg')
+ if new_args[1] == 'pl':
+ new_args[1] = '3pl'
+ output = pattern_es.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def verb_past_es(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(arg)
+ if len(new_args) < 2:
+ new_args.append('3sgp')
+ if new_args[1] == 'ppl':
+ new_args[1] = '3ppl'
+ output = pattern_es.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_plural_es(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if kwargs.get('noun_is_singular', False):
+ noun = pargs[0]
+ else:
+ noun = noun_singular_es(pargs[0])
+ if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
+ return str(noun)
+ output = pattern_es.pluralize(str(noun))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_singular_es(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
+ return pargs[0]
+ output = pattern_es.singularize(str(pargs[0]))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def indefinite_article_es(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ output = pattern_es.article(str(pargs[0]).lower()) + " " + str(pargs[0])
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
diff --git a/docassemble_base/docassemble/base/language/language_fr.py b/docassemble_base/docassemble/base/language/language_fr.py
new file mode 100644
index 000000000..4af6457eb
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/language_fr.py
@@ -0,0 +1,65 @@
+from docassemble.base.pattern import pattern_fr
+from .core import ensure_definition
+from .capitalization import capitalize
+from .numbers import number_or_length
+
+def verb_present_fr(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(str(arg))
+ if len(new_args) < 2:
+ new_args.append('3sg')
+ if new_args[1] == 'pl':
+ new_args[1] = '3pl'
+ output = pattern_fr.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def verb_past_fr(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(arg)
+ if len(new_args) < 2:
+ new_args.append('3sgp')
+ if new_args[1] == 'ppl':
+ new_args[1] = '3ppl'
+ output = pattern_fr.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_plural_fr(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if kwargs.get('noun_is_singular', False):
+ noun = pargs[0]
+ else:
+ noun = noun_singular_fr(pargs[0])
+ if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
+ return str(noun)
+ output = pattern_fr.pluralize(str(noun))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_singular_fr(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
+ return pargs[0]
+ output = pattern_fr.singularize(str(pargs[0]))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def indefinite_article_fr(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ output = pattern_fr.article(str(pargs[0]).lower()) + " " + str(pargs[0])
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
diff --git a/docassemble_base/docassemble/base/language/language_it.py b/docassemble_base/docassemble/base/language/language_it.py
new file mode 100644
index 000000000..c517fa844
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/language_it.py
@@ -0,0 +1,65 @@
+from docassemble.base.pattern import pattern_it
+from .core import ensure_definition
+from .capitalization import capitalize
+from .numbers import number_or_length
+
+def verb_present_it(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(str(arg))
+ if len(new_args) < 2:
+ new_args.append('3sg')
+ if new_args[1] == 'pl':
+ new_args[1] = '3pl'
+ output = pattern_it.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def verb_past_it(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(arg)
+ if len(new_args) < 2:
+ new_args.append('3sgp')
+ if new_args[1] == 'ppl':
+ new_args[1] = '3ppl'
+ output = pattern_it.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_plural_it(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if kwargs.get('noun_is_singular', False):
+ noun = pargs[0]
+ else:
+ noun = noun_singular_it(pargs[0])
+ if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
+ return str(noun)
+ output = pattern_it.pluralize(str(noun))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_singular_it(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
+ return pargs[0]
+ output = pattern_it.singularize(str(pargs[0]))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def indefinite_article_it(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ output = pattern_it.article(str(pargs[0]).lower()) + " " + str(pargs[0])
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
diff --git a/docassemble_base/docassemble/base/language/language_nl.py b/docassemble_base/docassemble/base/language/language_nl.py
new file mode 100644
index 000000000..6671eb3e9
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/language_nl.py
@@ -0,0 +1,65 @@
+from docassemble.base.pattern import pattern_nl
+from .core import ensure_definition
+from .capitalization import capitalize
+from .numbers import number_or_length
+
+def verb_present_nl(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(str(arg))
+ if len(new_args) < 2:
+ new_args.append('3sg')
+ if new_args[1] == 'pl':
+ new_args[1] = '3pl'
+ output = pattern_nl.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def verb_past_nl(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ new_args = []
+ for arg in pargs:
+ new_args.append(arg)
+ if len(new_args) < 2:
+ new_args.append('3sgp')
+ if new_args[1] == 'ppl':
+ new_args[1] = '3ppl'
+ output = pattern_nl.conjugate(*new_args, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_plural_nl(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if kwargs.get('noun_is_singular', False):
+ noun = pargs[0]
+ else:
+ noun = noun_singular_nl(pargs[0])
+ if len(pargs) >= 2 and number_or_length(pargs[1]) == 1:
+ return str(noun)
+ output = pattern_nl.pluralize(str(noun))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def noun_singular_nl(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ if len(pargs) >= 2 and number_or_length(pargs[1]) != 1:
+ return pargs[0]
+ output = pattern_nl.singularize(str(pargs[0]))
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
+
+
+def indefinite_article_nl(*pargs, **kwargs):
+ ensure_definition(*pargs, **kwargs)
+ output = pattern_nl.article(str(pargs[0]).lower()) + " " + str(pargs[0])
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(output)
+ return output
diff --git a/docassemble_base/docassemble/base/language/numbers.py b/docassemble_base/docassemble/base/language/numbers.py
new file mode 100644
index 000000000..9ea728067
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/numbers.py
@@ -0,0 +1,203 @@
+import decimal
+import locale
+from typing import Callable
+import num2words
+from docassemble.base.thread_context import this_thread
+from .capitalization import capitalize
+from .control import get_language
+from .core import (
+ language_function_constructor,
+ ensure_definition,
+ update_language_function,
+)
+
+ordinal_functions: dict[str, Callable] = {}
+
+ordinal_numbers: dict[str, dict[str, str]] = {}
+
+nice_numbers: dict[str, dict[str, str]] = {}
+
+ordinal_number = language_function_constructor('ordinal_number')
+ordinal = language_function_constructor('ordinal')
+nice_number = language_function_constructor('nice_number')
+
+if ordinal_number.__doc__ is None:
+ ordinal_number.__doc__ = """Return the ordinal form of a cardinal number.
+
+ Args:
+ num: The cardinal number (1-based).
+ **kwargs: Optional keyword arguments including ``capitalize``
+ (bool) and ``use_word`` (bool, default depends on the value).
+
+ Returns:
+ str: The ordinal form (e.g. ``ordinal_number(8)`` returns
+ ``'eighth'``; ``ordinal_number(8, use_word=False)`` returns
+ ``'8th'``).
+ """
+if ordinal.__doc__ is None:
+ ordinal.__doc__ = """Return the ordinal form of a zero-based index.
+
+ Equivalent to ``ordinal_number(num + 1)``. This is useful when working
+ with zero-based list indexes.
+
+ Args:
+ num: The zero-based index.
+ **kwargs: Optional keyword arguments passed to :func:`ordinal_number`.
+
+ Returns:
+ str: The ordinal form (e.g. ``ordinal(0)`` returns ``'first'``;
+ ``ordinal(22)`` returns ``'23rd'``).
+ """
+
+def string_to_number(number):
+ try:
+ float_number = float(number)
+ int_number = int(number)
+ if float_number == int_number:
+ return int_number
+ return float_number
+ except:
+ return number
+
+
+def number_to_word(number, **kwargs):
+ language = kwargs.get('language', None)
+ capitalize_arg = kwargs.get('capitalize', False)
+ function = kwargs.get('function', None)
+ raise_on_error = kwargs.get('raise_on_error', False)
+ if function not in ('ordinal', 'ordinal_num'):
+ function = 'cardinal'
+ if language is None:
+ language = get_language()
+ for lang, loc in (('en', 'en_GB'), ('en', 'en_IN'), ('es', 'es_CO'), ('es', 'es_VE'), ('fr', 'fr_CH'), ('fr', 'fr_BE'), ('fr', 'fr_DZ'), ('pt', 'pt_BR')):
+ if language == lang and this_thread.locale.startswith(loc):
+ language = loc
+ break
+ number = string_to_number(number)
+ if raise_on_error:
+ the_word = num2words.num2words(number, lang=language, to=function)
+ else:
+ try:
+ the_word = num2words.num2words(number, lang=language, to=function)
+ except NotImplementedError:
+ the_word = str(number)
+ if capitalize_arg:
+ return capitalize(the_word)
+ return the_word
+
+
+def ordinal_default(the_number, **kwargs):
+ """Returns the "first," "second," "third," etc. for a given number, which is expected to
+ be an index starting with zero. ordinal(0) returns "first." For a more literal ordinal
+ number function, see ordinal_number()."""
+ result = ordinal_number(int(float(the_number)) + 1, **kwargs)
+ if 'capitalize' in kwargs and kwargs['capitalize']:
+ return capitalize(result)
+ return result
+
+
+def nice_number_default(the_number, **kwargs):
+ """Returns the number as a word in the current language."""
+ capitalize_arg = kwargs.get('capitalize', False)
+ language = kwargs.get('language', None)
+ use_word = kwargs.get('use_word', None)
+ ensure_definition(the_number, capitalize_arg, language)
+ if language is None:
+ language = this_thread.language
+ if language in nice_numbers:
+ language_to_use = language
+ elif '*' in nice_numbers:
+ language_to_use = '*'
+ else:
+ language_to_use = 'en'
+ if isinstance(the_number, float):
+ the_number = float(decimal.Context(prec=8).create_decimal_from_float(the_number))
+ if int(float(the_number)) == float(the_number):
+ the_number = int(float(the_number))
+ is_integer = True
+ else:
+ is_integer = False
+ if language_to_use in nice_numbers and str(the_number) in nice_numbers[language_to_use]:
+ the_word = nice_numbers[language_to_use][str(the_number)]
+ if capitalize_arg:
+ return capitalize(the_word)
+ return the_word
+ if use_word or (is_integer and 0 <= the_number < 11 and use_word is not False):
+ try:
+ return number_to_word(the_number, **kwargs)
+ except:
+ pass
+ if isinstance(the_number, int):
+ return str(locale.format_string("%d", the_number, grouping=True))
+ return str(locale.format_string("%.2f", float(the_number), grouping=True)).rstrip('0')
+
+
+def ordinal_function_en(i, **kwargs):
+ try:
+ i = int(i)
+ except:
+ i = 0
+ use_word = kwargs.get('use_word', None)
+ if use_word is True:
+ kwargs['function'] = 'ordinal'
+ elif use_word is False:
+ kwargs['function'] = 'ordinal_num'
+ else:
+ if i < 11:
+ kwargs['function'] = 'ordinal'
+ else:
+ kwargs['function'] = 'ordinal_num'
+ return number_to_word(i, **kwargs)
+
+
+def ordinal_number_default(the_number, **kwargs):
+ """Returns the "first," "second," "third," etc. for a given number.
+ ordinal_number(1) returns "first." For a function that can be used
+ on index numbers that start with zero, see ordinal()."""
+ num = str(the_number)
+ if kwargs.get('use_word', True):
+ if this_thread.language in ordinal_numbers and num in ordinal_numbers[this_thread.language]:
+ return ordinal_numbers[this_thread.language][num]
+ if '*' in ordinal_numbers and num in ordinal_numbers['*']:
+ return ordinal_numbers['*'][num]
+ if this_thread.language in ordinal_functions:
+ language_to_use = this_thread.language
+ elif '*' in ordinal_functions:
+ language_to_use = '*'
+ else:
+ language_to_use = 'en'
+ return ordinal_functions[language_to_use](the_number, **kwargs)
+
+
+def update_nice_numbers(lang, defs):
+ if lang not in nice_numbers:
+ nice_numbers[lang] = {}
+ for number, the_word in defs.items():
+ nice_numbers[lang][str(number)] = the_word
+
+
+def update_ordinal_numbers(lang, defs):
+ if lang not in ordinal_numbers:
+ ordinal_numbers[lang] = {}
+ for number, the_word in defs.items():
+ ordinal_numbers[lang][str(number)] = the_word
+
+
+def update_ordinal_function(lang, func):
+ ordinal_functions[lang] = func
+
+
+def number_or_length(target):
+ if isinstance(target, (int, float)):
+ return target
+ if isinstance(target, (list, dict, set, tuple)) or (hasattr(target, 'elements') and isinstance(target.elements, (list, dict, set))):
+ return len(target)
+ if target:
+ return 2
+ return 1
+
+
+update_ordinal_function('en', ordinal_function_en)
+update_ordinal_function('*', ordinal_function_en)
+update_language_function('*', 'ordinal_number', ordinal_number_default)
+update_language_function('*', 'ordinal', ordinal_default)
diff --git a/docassemble_base/docassemble/base/language/utils.py b/docassemble_base/docassemble/base/language/utils.py
new file mode 100644
index 000000000..3f8501e77
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/utils.py
@@ -0,0 +1,29 @@
+from docassemble.base.language.core import ensure_definition
+
+def fix_punctuation(text, mark=None, other_marks=None):
+ """Ensure the text ends with a punctuation mark, adding one if necessary.
+
+ Args:
+ text (str): The text to check.
+ mark (str, optional): The punctuation mark to append if none is
+ present. Defaults to ``'.'``.
+ other_marks (list, optional): A list of punctuation marks that are
+ considered acceptable endings. Defaults to ``['.', '?', '!']``.
+
+ Returns:
+ str: The text, possibly with a punctuation mark appended.
+ """
+ ensure_definition(text, mark, other_marks)
+ if other_marks is None:
+ other_marks = ['.', '?', '!']
+ if not isinstance(other_marks, list):
+ other_marks = list(other_marks)
+ if mark is None:
+ mark = '.'
+ text = text.rstrip()
+ if mark == '':
+ return text
+ for end_mark in set([mark] + other_marks):
+ if text.endswith(end_mark):
+ return text
+ return text + mark
diff --git a/docassemble_base/docassemble/base/language/words.py b/docassemble_base/docassemble/base/language/words.py
new file mode 100644
index 000000000..e62e99e87
--- /dev/null
+++ b/docassemble_base/docassemble/base/language/words.py
@@ -0,0 +1,75 @@
+from docassemble.base.thread_context import this_thread
+from .capitalization import capitalize
+
+word_collection = {
+ 'en': {
+ 'This field is required.': 'You need to fill this in.',
+ "Country Code": 'Country Code (e.g., "us")',
+ "First Subdivision": 'State Abbreviation (e.g., "NY")',
+ "Second Subdivision": "County",
+ "Third Subdivision": "Municipality",
+ }
+}
+
+
+def words():
+ return word_collection[this_thread.language]
+
+
+class LazyWord:
+
+ def __init__(self, *args, **kwargs):
+ if len(kwargs) > 0:
+ self.original = args[0] % kwargs
+ else:
+ self.original = args[0]
+
+ def __mod__(self, other):
+ return word(self.original) % other
+
+ def __str__(self):
+ return word(self.original)
+
+
+def word(the_word, **kwargs):
+ """Return the word translated into the current language.
+
+ If no translation is found for the current language, the input is
+ returned unchanged. Used throughout docassemble to support
+ multilingual interviews.
+
+ Args:
+ the_word (str): The word or phrase to translate.
+ **kwargs: Optional keyword arguments. Pass ``language`` to
+ look up a translation for a specific language, or
+ ``capitalize=True`` to capitalize the result.
+
+ Returns:
+ str: The translated (or original) word.
+ """
+ # Currently, no kwargs are used, but in the future, this function could be
+ # expanded to use kwargs. For example, for languages with gendered words,
+ # the gender could be passed as a keyword argument.
+ if the_word is True:
+ the_word = 'yes'
+ elif the_word is False:
+ the_word = 'no'
+ elif the_word is None:
+ the_word = "I don't know"
+ if isinstance(the_word, LazyWord):
+ the_word = the_word.original
+ try:
+ the_word = word_collection[kwargs.get('language', this_thread.language)][the_word]
+ except:
+ the_word = str(the_word)
+ if kwargs.get('capitalize', False):
+ return capitalize(the_word)
+ return the_word
+
+
+def update_word_collection(lang, defs):
+ if lang not in word_collection:
+ word_collection[lang] = {}
+ for the_word, translation in defs.items():
+ if translation is not None:
+ word_collection[lang][the_word] = translation
diff --git a/docassemble_base/docassemble/base/legal.py b/docassemble_base/docassemble/base/legal.py
index 7e3a9317e..d2f07ad66 100644
--- a/docassemble_base/docassemble/base/legal.py
+++ b/docassemble_base/docassemble/base/legal.py
@@ -1,7 +1,278 @@
+# ruff: noqa: F401
+# pylint: disable=unused-import
from itertools import chain
-from docassemble.base.functions import alpha, roman, item_label, comma_and_list, get_language, set_language, get_dialect, get_voice, set_country, get_country, word, comma_list, ordinal, ordinal_number, need, nice_number, quantity_noun, possessify, verb_past, verb_present, noun_plural, noun_singular, space_to_underscore, force_ask, force_gather, period_list, name_suffix, currency, currency_symbol, indefinite_article, nodoublequote, capitalize, title_case, url_of, do_you, did_you, does_a_b, did_a_b, your, her, his, is_word, get_locale, set_locale, update_locale, process_action, url_action, get_info, set_info, get_config, prevent_going_back, qr_code, action_menu_item, from_b64_json, defined, define, value, message, response, json_response, command, single_paragraph, quote_paragraphs, location_returned, location_known, user_lat_lon, interview_url, interview_url_action, interview_url_as_qr, interview_url_action_as_qr, interview_email, get_emails, get_default_timezone, user_logged_in, interface, user_privileges, user_has_privilege, user_info, current_context, action_arguments, action_argument, background_action, background_response, background_response_action, background_error_action, us, set_live_help_status, chat_partners_available, phone_number_in_e164, phone_number_formatted, phone_number_is_valid, countries_list, country_name, write_record, read_records, delete_record, variables_as_json, all_variables, language_from_browser, device, plain, bold, italic, states_list, state_name, subdivision_type, indent, raw, fix_punctuation, set_progress, get_progress, referring_url, undefine, invalidate, dispatch, yesno, noyes, split, showif, showifdef, phone_number_part, set_parts, log, encode_name, decode_name, interview_list, interview_menu, server_capabilities, session_tags, get_chat_log, get_user_list, get_user_info, set_user_info, get_user_secret, create_user, invite_user, create_session, get_session_variables, set_session_variables, get_question_data, go_back_in_session, manage_privileges, redact, forget_result_of, re_run_logic, reconsider, set_title, set_save_status, single_to_double_newlines, verbatim, add_separators, store_variables_snapshot, update_terms, set_variables, language_name, run_action_in_session, static_image # noqa: F401 # pylint: disable=unused-import
-from docassemble.base.util import LatitudeLongitude, RoleChangeTracker, Name, IndividualName, Address, City, Event, Person, Thing, Individual, ChildList, FinancialList, PeriodicFinancialList, Income, Asset, Expense, Value, PeriodicValue, OfficeList, Organization, send_email, send_sms, send_fax, map_of, last_access_time, last_access_delta, last_access_days, last_access_hours, last_access_minutes, returning_user, timezone_list, as_datetime, current_datetime, date_difference, date_interval, today, month_of, day_of, dow_of, year_of, format_date, format_datetime, format_time, DARedis, DACloudStorage, DAGoogleAPI, SimpleTextMachineLearner, MachineLearningEntry, RandomForestMachineLearner, SVMMachineLearner, ocr_file, ocr_file_in_background, read_qr, get_sms_session, initiate_sms_session, terminate_sms_session, path_and_mimetype, run_python_module, pdf_concatenate, include_docx_template, start_time, zip_file, validation_error, DAValidationError, action_button_html, url_ask, overlay_pdf, DAStore, explain, clear_explanations, logic_explanation, set_status, get_status, DAWeb, DAWebError, json, re, iso_country, assemble_docx, docx_concatenate, task_performed, task_not_yet_performed, mark_task_as_performed, times_task_performed, set_task_counter, stash_data, retrieve_stashed_data, DABreadCrumbs, DAOAuth, DAObject, DAList, DADict, DAOrderedDict, DASet, DAFile, DAFileCollection, DAFileList, DAStaticFile, DAEmail, DAEmailRecipient, DAEmailRecipientList, DATemplate, DAEmpty, DALink, selections, objects_from_file, RelationshipTree, DAContext, DA, DAGlobal, transform_json_variables # noqa: F401 # pylint: disable=unused-import
-# from docassemble.base.logger import logmessage
+from docassemble.base.functions import (
+ alpha,
+ roman,
+ item_label,
+ comma_and_list,
+ get_language,
+ set_language,
+ get_dialect,
+ get_voice,
+ set_country,
+ get_country,
+ word,
+ comma_list,
+ ordinal,
+ ordinal_number,
+ need,
+ nice_number,
+ quantity_noun,
+ possessify,
+ verb_past,
+ verb_present,
+ noun_plural,
+ noun_singular,
+ space_to_underscore,
+ force_ask,
+ force_gather,
+ period_list,
+ name_suffix,
+ currency,
+ currency_symbol,
+ indefinite_article,
+ nodoublequote,
+ capitalize,
+ title_case,
+ url_of,
+ do_you,
+ did_you,
+ does_a_b,
+ did_a_b,
+ your,
+ her,
+ his,
+ is_word,
+ get_locale,
+ set_locale,
+ update_locale,
+ process_action,
+ url_action,
+ get_info,
+ set_info,
+ get_config,
+ prevent_going_back,
+ qr_code,
+ action_menu_item,
+ from_b64_json,
+ defined,
+ define,
+ value,
+ message,
+ response,
+ json_response,
+ command,
+ single_paragraph,
+ quote_paragraphs,
+ location_returned,
+ location_known,
+ user_lat_lon,
+ interview_url,
+ interview_url_action,
+ interview_url_as_qr,
+ interview_url_action_as_qr,
+ interview_email,
+ get_emails,
+ get_default_timezone,
+ user_logged_in,
+ interface,
+ user_privileges,
+ user_has_privilege,
+ user_info,
+ current_context,
+ action_arguments,
+ action_argument,
+ background_action,
+ background_response,
+ background_response_action,
+ background_error_action,
+ us,
+ set_live_help_status,
+ chat_partners_available,
+ phone_number_in_e164,
+ phone_number_formatted,
+ phone_number_is_valid,
+ countries_list,
+ country_name,
+ write_record,
+ read_records,
+ delete_record,
+ variables_as_json,
+ all_variables,
+ language_from_browser,
+ device,
+ plain,
+ bold,
+ italic,
+ states_list,
+ state_name,
+ subdivision_type,
+ indent,
+ raw,
+ fix_punctuation,
+ set_progress,
+ get_progress,
+ referring_url,
+ undefine,
+ invalidate,
+ dispatch,
+ yesno,
+ noyes,
+ split,
+ showif,
+ showifdef,
+ phone_number_part,
+ set_parts,
+ log,
+ encode_name,
+ decode_name,
+ interview_list,
+ interview_menu,
+ server_capabilities,
+ session_tags,
+ get_chat_log,
+ get_user_list,
+ get_user_info,
+ set_user_info,
+ get_user_secret,
+ create_user,
+ invite_user,
+ create_session,
+ get_session_variables,
+ set_session_variables,
+ get_question_data,
+ go_back_in_session,
+ manage_privileges,
+ redact,
+ forget_result_of,
+ re_run_logic,
+ reconsider,
+ set_title,
+ set_save_status,
+ single_to_double_newlines,
+ verbatim,
+ add_separators,
+ store_variables_snapshot,
+ update_terms,
+ set_variables,
+ language_name,
+ run_action_in_session,
+ static_image,
+)
+from docassemble.base.util import (
+ LatitudeLongitude,
+ RoleChangeTracker,
+ Name,
+ IndividualName,
+ Address,
+ City,
+ Event,
+ Person,
+ Thing,
+ Individual,
+ ChildList,
+ FinancialList,
+ PeriodicFinancialList,
+ Income,
+ Asset,
+ Expense,
+ Value,
+ PeriodicValue,
+ OfficeList,
+ Organization,
+ send_email,
+ send_sms,
+ send_fax,
+ map_of,
+ last_access_time,
+ last_access_delta,
+ last_access_days,
+ last_access_hours,
+ last_access_minutes,
+ returning_user,
+ timezone_list,
+ as_datetime,
+ current_datetime,
+ date_difference,
+ date_interval,
+ today,
+ month_of,
+ day_of,
+ dow_of,
+ year_of,
+ format_date,
+ format_datetime,
+ format_time,
+ DARedis,
+ DACloudStorage,
+ DAGoogleAPI,
+ SimpleTextMachineLearner,
+ MachineLearningEntry,
+ RandomForestMachineLearner,
+ SVMMachineLearner,
+ ocr_file,
+ ocr_file_in_background,
+ read_qr,
+ get_sms_session,
+ initiate_sms_session,
+ terminate_sms_session,
+ path_and_mimetype,
+ run_python_module,
+ pdf_concatenate,
+ include_docx_template,
+ start_time,
+ zip_file,
+ validation_error,
+ DAValidationError,
+ action_button_html,
+ url_ask,
+ overlay_pdf,
+ DAStore,
+ explain,
+ clear_explanations,
+ logic_explanation,
+ set_status,
+ get_status,
+ DAWeb,
+ DAWebError,
+ json,
+ re,
+ iso_country,
+ assemble_docx,
+ docx_concatenate,
+ task_performed,
+ task_not_yet_performed,
+ mark_task_as_performed,
+ times_task_performed,
+ set_task_counter,
+ stash_data,
+ retrieve_stashed_data,
+ DABreadCrumbs,
+ DAOAuth,
+ DAObject,
+ DAList,
+ DADict,
+ DAOrderedDict,
+ DASet,
+ DAFile,
+ DAFileCollection,
+ DAFileList,
+ DAStaticFile,
+ DAEmail,
+ DAEmailRecipient,
+ DAEmailRecipientList,
+ DATemplate,
+ DAEmpty,
+ DALink,
+ selections,
+ objects_from_file,
+ RelationshipTree,
+ DAContext,
+ DA,
+ DAGlobal,
+ transform_json_variables,
+)
__all__ = [
'alpha',
@@ -353,8 +624,8 @@ def init(self, *pargs, **kwargs):
self.initializeAttribute('court', self.CourtClass)
self.initializeAttribute('defendant', self.PartyListClass)
self.initializeAttribute('plaintiff', self.PartyListClass)
- self.firstParty = self.plaintiff
- self.secondParty = self.defendant
+ self.firstParty = self.plaintiff # pylint: disable=invalid-name
+ self.secondParty = self.defendant # pylint: disable=invalid-name
self.is_solo_action = False
self.state = None
self.action_type = 'plaintiff defendant'
diff --git a/docassemble_base/docassemble/base/mako/ast.py b/docassemble_base/docassemble/base/mako/ast.py
index e8fec80a8..fb6c0a41c 100644
--- a/docassemble_base/docassemble/base/mako/ast.py
+++ b/docassemble_base/docassemble/base/mako/ast.py
@@ -11,7 +11,7 @@
from docassemble.base.mako import exceptions
from docassemble.base.mako import pyparser
-from docassemble.base.astparser import myvisitnode
+from docassemble.base.astparser import MyVisitNode
from docassemble.base.astparser import ast as base_ast
@@ -21,7 +21,7 @@ class PythonCode:
def __init__(self, code, **exception_kwargs):
if isinstance(code, str):
- myvisitor = myvisitnode()
+ myvisitor = MyVisitNode()
t = base_ast.parse(code.strip())
myvisitor.visit(t)
self.names_used = set()
diff --git a/docassemble_base/docassemble/base/mako/exceptions.py b/docassemble_base/docassemble/base/mako/exceptions.py
index c353f4945..fbe2e9154 100644
--- a/docassemble_base/docassemble/base/mako/exceptions.py
+++ b/docassemble_base/docassemble/base/mako/exceptions.py
@@ -161,7 +161,7 @@ def _init(self, trcback):
(line_map, template_lines, template_filename) = mods[filename]
except KeyError:
try:
- info = mako.template._get_module_info(filename)
+ info = docassemble.base.mako.template._get_module_info(filename)
module_source = info.code
template_source = info.source
template_filename = (
@@ -185,7 +185,7 @@ def _init(self, trcback):
template_ln = 1
- mtm = mako.template.ModuleInfo
+ mtm = docassemble.base.mako.template.ModuleInfo
source_map = mtm.get_module_source_metadata(
module_source, full_line_map=True
)
diff --git a/docassemble_base/docassemble/base/microsoft.py b/docassemble_base/docassemble/base/microsoft.py
index 1e8a43fe5..6f0d8cf4c 100644
--- a/docassemble_base/docassemble/base/microsoft.py
+++ b/docassemble_base/docassemble/base/microsoft.py
@@ -5,7 +5,12 @@
import re
import logging
import yaml
-from azure.storage.blob import BlobServiceClient, BlobSasPermissions, ContentSettings, generate_blob_sas
+from azure.storage.blob import (
+ BlobServiceClient,
+ BlobSasPermissions,
+ ContentSettings,
+ generate_blob_sas,
+)
from azure.identity import ManagedIdentityCredential
from azure.keyvault.secrets import SecretClient
from docassemble.base.error import DAException
@@ -16,7 +21,7 @@
epoch = datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
-class azureobject:
+class AzureObject:
def __init__(self, azure_config):
if ('key vault name' in azure_config and azure_config['key vault name'] is not None and 'managed identity' in azure_config and azure_config['managed identity'] is not None):
@@ -43,7 +48,7 @@ def __init__(self, azure_config):
raise DAException("Cannot connect to Azure without account name, account key, and container specified")
def get_key(self, key_name):
- new_key = azurekey(self, key_name, load=False)
+ new_key = AzureKey(self, key_name, load=False)
if new_key.exists():
new_key.get_properties()
new_key.does_exist = True
@@ -54,17 +59,17 @@ def get_key(self, key_name):
def search_key(self, key_name):
for blob in self.container_client.list_blobs(name_starts_with=key_name):
if blob.name == key_name:
- return azurekey(self, blob.name)
+ return AzureKey(self, blob.name)
return None
def list_keys(self, prefix):
output = []
for blob in self.container_client.list_blobs(name_starts_with=prefix):
- output.append(azurekey(self, blob.name))
+ output.append(AzureKey(self, blob.name))
return output
def get_secret(self, key_vault_reference):
- new_secret = azuresecret(self, key_vault_reference)
+ new_secret = AzureSecret(self, key_vault_reference)
return new_secret.get_secret_as_string()
def replace_secrets(self, match):
@@ -78,7 +83,7 @@ def load_with_secrets(self, config):
return loaded_config_with_secrets
-class azurekey:
+class AzureKey:
def __init__(self, azure_object, key_name, load=True):
self.azure_object = azure_object
@@ -161,7 +166,7 @@ def generate_url(self, seconds, display_filename=None, content_type=None, inline
return self.blob_client.url + '?' + token
-class azuresecret:
+class AzureSecret:
def __init__(self, azure_object, key_vault_reference):
self.azure_object = azure_object
diff --git a/docassemble_base/docassemble/base/pandoc.py b/docassemble_base/docassemble/base/pandoc.py
index 747c95406..daf99a332 100644
--- a/docassemble_base/docassemble/base/pandoc.py
+++ b/docassemble_base/docassemble/base/pandoc.py
@@ -1,3 +1,4 @@
+# pylint: disable=global-statement, consider-using-with
import os
import os.path
import subprocess
@@ -13,13 +14,18 @@
import convertapi
import requests
from pikepdf import Pdf
-import docassemble.base.filter
-import docassemble.base.functions
from docassemble.base.config import daconfig
+from docassemble.base.error import DAError, DAException
+from docassemble.base.filter.docx import docx_filter
+from docassemble.base.filter.pandoc import pdf_filter, rtf_prefilter, rtf_filter
+from docassemble.base.functions import (
+ package_template_filename,
+ standard_template_filename,
+)
+from docassemble.base.hooks import secure_filename, applock
from docassemble.base.logger import logmessage
from docassemble.base.pdfa import pdf_to_pdfa
from docassemble.base.pdftk import pdf_encrypt
-from docassemble.base.error import DAError, DAException
style_find = re.compile(r'{\s*(\\s([1-9])[^\}]+)\\sbasedon[^\}]+heading ([0-9])', flags=re.DOTALL)
@@ -32,7 +38,6 @@
PANDOC_ENGINE = '--pdf-engine=' + daconfig.get('pandoc engine', 'pdflatex')
if daconfig.get('pandoc with celery', False):
PANDOC_MODE = REMOTE
- from docassemble.pandoc.tasks import run_pandoc # pylint: disable=import-error,no-name-in-module
elif PANDOC_PATH and shutil.which(PANDOC_PATH):
PANDOC_MODE = LOCAL
else:
@@ -42,7 +47,6 @@
LIBREOFFICE_INITIALIZED = False
if daconfig.get('libreoffice with celery', False):
LIBREOFFICE_MODE = REMOTE
- from docassemble.libreoffice.tasks import run_libreoffice # pylint: disable=import-error,no-name-in-module
elif LIBREOFFICE_PATH and shutil.which(PANDOC_PATH):
LIBREOFFICE_MODE = LOCAL
else:
@@ -125,7 +129,7 @@ def cloudconvert_to_pdf(in_format, from_file, to_file, pdfa, password):
uploaded = True
if not uploaded:
raise DAException("cloudconvert_to_pdf: failed to upload")
- r = requests.get("https://sync.api.cloudconvert.com/v2/jobs/%s" % (resp['data']['id'],), headers=headers, timeout=60)
+ r = requests.get(f"https://sync.api.cloudconvert.com/v2/jobs/{resp['data']['id']}", headers=headers, timeout=60)
wait_resp = r.json()
if 'data' not in wait_resp:
logmessage("cloudconvert_to_pdf: wait returned " + repr(r.text))
@@ -147,12 +151,12 @@ def convertapi_to_pdf(from_file, to_file):
def get_pandoc_version():
- p = subprocess.Popen(
+ with subprocess.Popen(
[PANDOC_PATH, '--version'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE
- )
- version_content = p.communicate()[0].decode('utf-8')
+ ) as p:
+ version_content = p.communicate()[0].decode('utf-8')
version_content = re.sub(r'\n.*', '', version_content)
version_content = re.sub(r'^pandoc ', '', version_content)
return version_content
@@ -164,8 +168,8 @@ def initialize_pandoc():
global PANDOC_INITIALIZED
if PANDOC_INITIALIZED:
return
- PANDOC_VERSION = get_pandoc_version()
- if PANDOC_VERSION.startswith('1'):
+ pandoc_version = get_pandoc_version()
+ if pandoc_version.startswith('1'):
PANDOC_OLD = True
PANDOC_ENGINE = '--latex-engine=' + daconfig.get('pandoc engine', 'pdflatex')
else:
@@ -241,21 +245,21 @@ def convert_to_file(self, question):
else:
self.output_extension = self.output_format
if self.output_format in ('rtf', 'rtf to docx') and self.template_file is None:
- self.template_file = docassemble.base.functions.standard_template_filename('Legal-Template.rtf')
+ self.template_file = standard_template_filename('Legal-Template.rtf')
if self.output_format == 'docx' and self.reference_file is None:
- self.reference_file = docassemble.base.functions.standard_template_filename('Legal-Template.docx')
+ self.reference_file = standard_template_filename('Legal-Template.docx')
if self.output_format in ('pdf', 'tex') and self.template_file is None:
- self.template_file = docassemble.base.functions.standard_template_filename('Legal-Template.tex')
+ self.template_file = standard_template_filename('Legal-Template.tex')
yaml_to_use = []
if self.output_format in ('rtf', 'rtf to docx'):
# logmessage("pre input content is " + str(self.input_content))
- self.input_content = docassemble.base.filter.rtf_prefilter(self.input_content)
+ self.input_content = rtf_prefilter(self.input_content)
# logmessage("post input content is " + str(self.input_content))
if self.output_format == 'docx':
- self.input_content = docassemble.base.filter.docx_filter(self.input_content, metadata=metadata_as_dict, question=question)
+ self.input_content = docx_filter(self.input_content, metadata=metadata_as_dict, question=question)
if self.output_format in ('pdf', 'tex'):
if len(self.initial_yaml) == 0:
- standard_file = docassemble.base.functions.standard_template_filename('Legal-Template.yml')
+ standard_file = standard_template_filename('Legal-Template.yml')
if standard_file is not None:
self.initial_yaml.append(standard_file)
for yaml_file in self.initial_yaml:
@@ -265,7 +269,7 @@ def convert_to_file(self, question):
if yaml_file is not None:
yaml_to_use.append(yaml_file)
# logmessage("Before: " + repr(self.input_content))
- self.input_content = docassemble.base.filter.pdf_filter(self.input_content, metadata=metadata_as_dict, question=question)
+ self.input_content = pdf_filter(self.input_content, metadata=metadata_as_dict, question=question)
# logmessage("After: " + repr(self.input_content))
if not re.search(r'[^\s]', self.input_content):
self.input_content = "\\textbf{}\n"
@@ -281,7 +285,7 @@ def convert_to_file(self, question):
raise DAException("Could not create latex conversion directory")
icc_profile_in_temp = os.path.join(tempfile.gettempdir(), 'sRGB_IEC61966-2-1_black_scaled.icc')
if not os.path.isfile(icc_profile_in_temp):
- shutil.copyfile(docassemble.base.functions.standard_template_filename('sRGB_IEC61966-2-1_black_scaled.icc'), icc_profile_in_temp)
+ shutil.copyfile(standard_template_filename('sRGB_IEC61966-2-1_black_scaled.icc'), icc_profile_in_temp)
if PANDOC_MODE not in (LOCAL, REMOTE):
raise DAException('LibreOffice is not available')
subprocess_arguments = [PANDOC_PATH, PANDOC_ENGINE]
@@ -291,12 +295,12 @@ def convert_to_file(self, question):
if len(yaml_to_use) > 0:
subprocess_arguments.extend(yaml_to_use)
if self.template_file is not None:
- subprocess_arguments.extend(['--template=%s' % self.template_file])
+ subprocess_arguments.extend([f'--template={self.template_file}'])
if self.reference_file is not None:
if PANDOC_OLD:
- subprocess_arguments.extend(['--reference-docx=%s' % self.reference_file])
+ subprocess_arguments.extend([f'--reference-docx={self.reference_file}'])
else:
- subprocess_arguments.extend(['--reference-doc=%s' % self.reference_file])
+ subprocess_arguments.extend([f'--reference-doc={self.reference_file}'])
if self.output_format in ('pdf', 'tex'):
subprocess_arguments.extend(['--from=markdown+raw_tex-latex_macros'])
subprocess_arguments.extend(['-s', '-o', temp_outfile.name])
@@ -307,9 +311,10 @@ def convert_to_file(self, question):
try:
msg = subprocess.check_output(subprocess_arguments, cwd=tempfile.gettempdir(), stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
except subprocess.CalledProcessError as err:
- raise DAException("Failed to assemble file: " + err.output.decode())
+ raise DAException("Failed to assemble file: " + err.output.decode()) from err
elif PANDOC_MODE == REMOTE:
- result = run_pandoc.delay(subprocess_arguments[2:], tempfile.gettempdir(), mode=0).get(disable_sync_subtasks=False)
+ from docassemble.pandoc.tasks import run_pandoc # pylint: disable=import-error,no-name-in-module
+ result = run_pandoc.delay(subprocess_arguments[2:], tempfile.gettempdir(), mode=0).get(disable_sync_subtasks=False) # pylint: disable=possibly-used-before-assignment
if result.ok:
msg = result.content
else:
@@ -325,7 +330,7 @@ def convert_to_file(self, question):
file_contents = the_file.read()
# with open('/tmp/asdf.rtf', 'w') as deb_file:
# deb_file.write(file_contents)
- file_contents = docassemble.base.filter.rtf_filter(file_contents, metadata=metadata_as_dict, styles=get_rtf_styles(self.template_file), question=question)
+ file_contents = rtf_filter(file_contents, metadata=metadata_as_dict, styles=get_rtf_styles(self.template_file), question=question)
with open(temp_outfile.name, "wb") as the_file:
the_file.write(bytearray(file_contents, encoding='utf-8'))
if self.output_format == 'rtf to docx':
@@ -342,7 +347,7 @@ def convert_to_file(self, question):
if self.output_format == 'pdf' and (self.password or self.owner_password):
pdf_encrypt(self.output_filename, self.password, self.owner_password)
else:
- raise IOError("Failed creating file: %s" % temp_outfile.name)
+ raise IOError(f"Failed creating file: {temp_outfile.name}")
def convert(self, question):
latex_conversion_directory = os.path.join(tempfile.gettempdir(), 'conv')
@@ -365,23 +370,24 @@ def convert(self, question):
input_format = "markdown+smart"
if self.output_format in ('pdf', 'tex'):
input_format += '+raw_tex-latex_macros'
- subprocess_arguments.extend(['-M', 'latextmpdir=' + os.path.join('.', 'conv'), '--from=%s' % input_format, '--to=%s' % self.output_format])
+ subprocess_arguments.extend(['-M', 'latextmpdir=' + os.path.join('.', 'conv'), f'--from={input_format}', f'--to={self.output_format}'])
if self.output_format == 'html':
subprocess_arguments.append('--ascii')
subprocess_arguments.extend(self.arguments)
# logmessage("Arguments are " + str(subprocess_arguments))
if PANDOC_MODE == LOCAL:
- p = subprocess.Popen(
+ self.output_filename = None
+ with subprocess.Popen(
subprocess_arguments,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
cwd=tempfile.gettempdir()
- )
- self.output_filename = None
- self.output_content = p.communicate(bytearray(self.input_content, encoding='utf-8'))[0]
+ ) as p:
+ self.output_content = p.communicate(bytearray(self.input_content, encoding='utf-8'))[0]
self.output_content = self.output_content.decode()
elif PANDOC_MODE == REMOTE:
self.output_filename = None
+ from docassemble.pandoc.tasks import run_pandoc # pylint: disable=import-error,no-name-in-module
result = run_pandoc.delay(subprocess_arguments[2:], tempfile.gettempdir(), input_content=self.input_content, mode=1).get(disable_sync_subtasks=False)
if result.ok:
self.output_content = result.content
@@ -394,7 +400,7 @@ def convert(self, question):
def word_to_pdf(in_file, in_format, out_file, pdfa=False, password=None, owner_password=None, update_refs=False, tagged=False, filename=None, retry=True):
if filename is None:
filename = 'file'
- filename = docassemble.base.functions.secure_filename(filename)
+ filename = secure_filename(filename)
tempdir = tempfile.mkdtemp(prefix='SavedFile')
from_file = os.path.join(tempdir, "file." + in_format)
to_file = os.path.join(tempdir, "file.pdf")
@@ -510,7 +516,7 @@ def word_to_pdf(in_file, in_format, out_file, pdfa=False, password=None, owner_p
if use_libreoffice:
start_time = time.time()
if UNOCONV_AVAILABLE:
- docassemble.base.functions.server.applock('obtain', 'unoconv', maxtime=6)
+ applock('obtain', 'unoconv', maxtime=6)
logmessage("Trying unoconv with " + repr(subprocess_arguments))
try:
completed_process = subprocess.run(subprocess_arguments, cwd=tempdir, timeout=120, check=False, capture_output=True)
@@ -519,10 +525,10 @@ def word_to_pdf(in_file, in_format, out_file, pdfa=False, password=None, owner_p
logmessage("word_to_pdf: unoconv took too long")
result = 1
tries = 5
- docassemble.base.functions.server.applock('release', 'unoconv', maxtime=6)
+ applock('release', 'unoconv', maxtime=6)
logmessage("Finished unoconv after {:.4f} seconds.".format(time.time() - start_time))
elif UNOCONVERT_AVAILABLE:
- docassemble.base.functions.server.applock('obtain', 'unoconvert', maxtime=6)
+ applock('obtain', 'unoconvert', maxtime=6)
logmessage("Trying unoconvert with " + repr(subprocess_arguments))
try:
completed_process = subprocess.run(subprocess_arguments, cwd=tempdir, timeout=120, check=False, capture_output=True)
@@ -531,12 +537,12 @@ def word_to_pdf(in_file, in_format, out_file, pdfa=False, password=None, owner_p
logmessage("word_to_pdf: unoconvert took too long")
result = 1
tries = 5
- docassemble.base.functions.server.applock('release', 'unoconvert', maxtime=6)
+ applock('release', 'unoconvert', maxtime=6)
logmessage("Finished unoconvert after {:.4f} seconds.".format(time.time() - start_time))
elif LIBREOFFICE_MODE == LOCAL:
initialize_libreoffice()
logmessage("Trying libreoffice with " + repr(subprocess_arguments))
- docassemble.base.functions.server.applock('obtain', 'libreoffice')
+ applock('obtain', 'libreoffice')
logmessage("Obtained libreoffice lock after {:.4f} seconds.".format(time.time() - start_time))
try:
completed_process = subprocess.run(subprocess_arguments, cwd=tempdir, timeout=120, check=False, capture_output=True)
@@ -546,9 +552,10 @@ def word_to_pdf(in_file, in_format, out_file, pdfa=False, password=None, owner_p
result = 1
tries = 5
logmessage("Finished libreoffice after {:.4f} seconds.".format(time.time() - start_time))
- docassemble.base.functions.server.applock('release', 'libreoffice')
+ applock('release', 'libreoffice')
elif LIBREOFFICE_MODE == REMOTE:
- result = run_libreoffice.delay(subprocess_arguments[1:], tempfile.gettempdir()).get(disable_sync_subtasks=False)
+ from docassemble.libreoffice.tasks import run_libreoffice # pylint: disable=import-error,no-name-in-module
+ result = run_libreoffice.delay(subprocess_arguments[1:], tempfile.gettempdir()).get(disable_sync_subtasks=False) # pylint: disable=possibly-used-before-assignment
if result == 1234:
result = 1
tries = 5
@@ -638,17 +645,18 @@ def rtf_to_docx(in_file, out_file):
if result != 0:
logmessage("rtf_to_docx: call to unoconvert returned non-zero response")
elif LIBREOFFICE_MODE == LOCAL:
- docassemble.base.functions.server.applock('obtain', 'libreoffice')
+ applock('obtain', 'libreoffice')
try:
result = subprocess.run(subprocess_arguments, cwd=tempdir, timeout=120, check=False).returncode
except subprocess.TimeoutExpired:
logmessage("rtf_to_docx: call to LibreOffice took too long")
result = 1
tries = 5
- docassemble.base.functions.server.applock('release', 'libreoffice')
+ applock('release', 'libreoffice')
if result != 0:
logmessage("rtf_to_docx: call to LibreOffice returned non-zero response")
else:
+ from docassemble.libreoffice.tasks import run_libreoffice # pylint: disable=import-error,no-name-in-module
result = run_libreoffice.delay(subprocess_arguments[1:], tempfile.gettempdir()).get(disable_sync_subtasks=False)
if result == 1234:
result = 1
@@ -712,17 +720,18 @@ def convert_file(in_file, out_file, input_extension, output_extension):
if result != 0:
logmessage("convert_file: call to unoconvert returned non-zero response")
elif LIBREOFFICE_MODE == LOCAL:
- docassemble.base.functions.server.applock('obtain', 'libreoffice')
+ applock('obtain', 'libreoffice')
try:
result = subprocess.run(subprocess_arguments, cwd=tempdir1, timeout=120, check=False).returncode
except subprocess.TimeoutExpired:
logmessage("convert_file: libreoffice took too long")
result = 1
tries = 5
- docassemble.base.functions.server.applock('release', 'libreoffice')
+ applock('release', 'libreoffice')
if result != 0:
logmessage("convert_file: call to LibreOffice returned non-zero response")
else:
+ from docassemble.libreoffice.tasks import run_libreoffice # pylint: disable=import-error,no-name-in-module
result = run_libreoffice.delay(subprocess_arguments[1:], tempfile.gettempdir()).get(disable_sync_subtasks=False)
if result == 1234:
result = 1
@@ -796,17 +805,18 @@ def word_to_markdown(in_file, in_format):
if result != 0:
logmessage("word_to_markdown: call to unoconvert returned non-zero response")
elif LIBREOFFICE_MODE == LOCAL:
- docassemble.base.functions.server.applock('obtain', 'libreoffice')
+ applock('obtain', 'libreoffice')
try:
result = subprocess.run(subprocess_arguments, cwd=tempdir, timeout=120, check=False).returncode
except subprocess.TimeoutExpired:
logmessage("word_to_markdown: libreoffice took too long")
result = 1
tries = 5
- docassemble.base.functions.server.applock('release', 'libreoffice')
+ applock('release', 'libreoffice')
if result != 0:
logmessage("word_to_markdown: call to LibreOffice returned non-zero response")
elif LIBREOFFICE_MODE == REMOTE:
+ from docassemble.libreoffice.tasks import run_libreoffice # pylint: disable=import-error,no-name-in-module
result = run_libreoffice.delay(subprocess_arguments[1:], tempfile.gettempdir()).get(disable_sync_subtasks=False)
if result == 1234:
result = 1
@@ -839,13 +849,14 @@ def word_to_markdown(in_file, in_format):
else:
if in_format_to_use == 'markdown':
in_format_to_use = "markdown+smart"
- subprocess_arguments.extend(['--from=%s' % str(in_format_to_use), '--to=markdown_phpextra', str(in_file_to_use), '-o', str(temp_file.name)])
+ subprocess_arguments.extend([f'--from={in_format_to_use}', '--to=markdown_phpextra', str(in_file_to_use), '-o', str(temp_file.name)])
if PANDOC_MODE == LOCAL:
try:
result = subprocess.run(subprocess_arguments, timeout=60, check=False).returncode
except subprocess.TimeoutExpired:
result = 1
elif PANDOC_MODE == REMOTE:
+ from docassemble.pandoc.tasks import run_pandoc # pylint: disable=import-error,no-name-in-module
result = run_pandoc.delay(subprocess_arguments[2:], tempfile.gettempdir(), mode=2).get(disable_sync_subtasks=False)
else:
raise DAException("Pandoc not installed.")
@@ -890,14 +901,15 @@ def update_references(filename):
tries = 0
while tries < 5:
if LIBREOFFICE_MODE == LOCAL:
- docassemble.base.functions.server.applock('obtain', 'libreoffice')
+ applock('obtain', 'libreoffice')
try:
result = subprocess.run(subprocess_arguments, cwd=tempfile.gettempdir(), timeout=120, check=False).returncode
except subprocess.TimeoutExpired:
result = 1
tries = 5
- docassemble.base.functions.server.applock('release', 'libreoffice')
+ applock('release', 'libreoffice')
else:
+ from docassemble.libreoffice.tasks import run_libreoffice # pylint: disable=import-error,no-name-in-module
result = run_libreoffice.delay(subprocess_arguments[1:], tempfile.gettempdir()).get(disable_sync_subtasks=False)
if result == 1234:
result = 1
@@ -922,11 +934,11 @@ def initialize_libreoffice():
if not os.path.isfile(LIBREOFFICE_MACRO_PATH):
logmessage("No LibreOffice macro path exists")
temp_file = tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".pdf")
- word_file = docassemble.base.functions.package_template_filename('docassemble.demo:data/templates/template_test.docx')
+ word_file = package_template_filename('docassemble.demo:data/templates/template_test.docx')
word_to_pdf(word_file, 'docx', temp_file.name, pdfa=False, password=None, owner_password=None, retry=False)
del temp_file
del word_file
- orig_path = docassemble.base.functions.package_template_filename('docassemble.base:data/macros/Module1.xba')
+ orig_path = package_template_filename('docassemble.base:data/macros/Module1.xba')
try:
assert os.path.isdir(os.path.dirname(LIBREOFFICE_MACRO_PATH))
# logmessage("Copying LibreOffice macro from " + orig_path)
diff --git a/docassemble_base/docassemble/base/parse.py b/docassemble_base/docassemble/base/parse.py
index 18ed5ca84..d2420a7c0 100644
--- a/docassemble_base/docassemble/base/parse.py
+++ b/docassemble_base/docassemble/base/parse.py
@@ -6,7 +6,6 @@
import os
import os.path
import sys
-import datetime
import time
import operator
import pprint
@@ -15,53 +14,116 @@
import array
import tempfile
import json
-import platform
import textwrap
from urllib.request import urlretrieve
from io import StringIO
-from collections import abc, OrderedDict, namedtuple
+from collections import abc, OrderedDict
from types import CodeType, FunctionType
import xml.etree.ElementTree as ET
from html.parser import HTMLParser
-from itertools import groupby, chain
import ruamel.yaml
-from jinja2 import ChainableUndefined
-from jinja2.runtime import StrictUndefined, UndefinedError
+from jinja2.runtime import UndefinedError
from jinja2.exceptions import TemplateError
-from jinja2.environment import Environment
-from jinja2 import FileSystemLoader, select_autoescape, TemplateNotFound
from jinja2 import meta as jinja2meta
-from jinja2.lexer import Token
-from jinja2.utils import internalcode, missing, object_type_repr
-from jinja2.ext import Extension
from docxtpl import DocxTemplate
import dateutil.parser
try:
import zoneinfo
except ImportError:
- from backports import zoneinfo
+ from backports import zoneinfo # type: ignore[no-redef]
from bs4 import BeautifulSoup
from docassemble_textstat.textstat import textstat
import qrcode
import qrcode.image.svg
-from docassemble.base import __version__ as da_version
-import docassemble.base.filter
-import docassemble.base.pdftk
-import docassemble.base.file_docx
-from docassemble.base.error import DAError, DANotFoundError, MandatoryQuestion, DAErrorNoEndpoint, DAErrorMissingVariable, ForcedNameError, QuestionError, ResponseError, BackgroundResponseError, BackgroundResponseActionError, CommandError, CodeExecute, DAValidationError, ForcedReRun, LazyNameError, DAAttributeError, DAIndexError, DAException, DANameError, DASourceError
-import docassemble.base.functions
-import docassemble.base.util
-from docassemble.base.functions import pickleable_objects, word, get_language, RawValue, get_config, safeyaml, altyaml, prettyyaml
-from docassemble.base.logger import logmessage
-from docassemble.base.pandoc import MyPandoc
-from docassemble.base.mako.template import Template as MakoTemplate
-from docassemble.base.mako.exceptions import SyntaxException, CompileException
-from docassemble.base.astparser import myvisitnode
+from .astparser import DetectIllegal
+from .astparser import MyVisitNode
+from .dates import format_date
+from .error import (
+ BackgroundResponseActionError,
+ BackgroundResponseError,
+ CodeExecute,
+ CommandError,
+ DAAttributeError,
+ DAError,
+ DAErrorMissingVariable,
+ DAErrorNoEndpoint,
+ DAException,
+ DAIndexError,
+ DANameError,
+ DANotFoundError,
+ DASourceError,
+ DAValidationError,
+ ForcedNameError,
+ ForcedReRun,
+ LazyNameError,
+ MandatoryQuestion,
+ QuestionError,
+ ResponseError,
+)
+from .interview_source import interview_source_from_string, InterviewSourceString
+from .file_docx import transform_for_docx, concatenate_files, fix_docx
+from .filter.html import (
+ emoji_html,
+ emoji_insert,
+ get_audio_urls,
+ get_video_urls,
+ markdown_to_html,
+)
+from .filter.utils import convert_svg_to_png
+from .functions import (
+ DANav,
+ RawValue,
+ altyaml,
+ custom_types,
+ get_action_stack,
+ get_config,
+ get_language,
+ intrinsic_name_of,
+ package_data_filename,
+ package_template_filename,
+ pickleable_objects,
+ pop_current_variable,
+ pop_event_stack,
+ prettyyaml,
+ reconsider,
+ reset_context,
+ reset_gathering_mode,
+ safeyaml,
+ serializable_dict,
+ set_context,
+ set_current_variable,
+ single_paragraph,
+ space_to_underscore,
+ static_filename_path,
+ undefine,
+ url_action,
+ word,
+ wrap_up,
+)
+from .helpers import extract_missing_name, fix_quotes
+from .hooks import (
+ file_finder,
+ get_default_language,
+ get_default_timezone,
+ get_main_page_parts,
+ save_numbered_file,
+ secure_filename_unicode_ok,
+ to_text,
+ url_finder,
+)
+from .jinja import custom_jinja_env
+from .language.control import set_language
+from .logger import logmessage
+from .mako.exceptions import SyntaxException, CompileException
+from .mako.template import Template as MakoTemplate
+from .pandoc import MyPandoc, word_to_pdf, update_references
+from .pdftk import fill_template
+from .thread_context import this_thread
+from .util import objects_from_structure, objects_from_data
equals_byte = bytes('=', 'utf-8')
RangeType = type(range(1, 2))
NoneType = type(None)
-da_arch = platform.machine()
standard_types = set(['integer', 'number', 'currency', 'float', 'file', 'files', 'range', 'multiselect', 'checkboxes', 'object_multiselect', 'object_checkboxes', 'user', 'camera', 'environment', 'date', 'datetime', 'time', 'email', 'microphone', 'ml', 'mlarea', 'noyes', 'noyesmaybe', 'noyesradio', 'noyeswide', 'yesno', 'yesnomaybe', 'yesnoradio', 'yesnowide', 'text', 'password', 'object'])
DEBUG = True
@@ -73,7 +135,6 @@
match_mako = re.compile(r'<%|\${|% if|% for|% while|\#\#')
emoji_match = re.compile(r':([^ ]+):')
valid_variable_match = re.compile(r'^[^\d][A-Za-z0-9\_]*$')
-nameerror_match = re.compile(r'\'(.*)\' (is not defined|referenced before assignment|is undefined|where it is not)')
document_match = re.compile(r'^--- *$', flags=re.MULTILINE)
remove_trailing_dots = re.compile(r'[\n\r]+\.\.\.$')
fix_tabs = re.compile(r'\t')
@@ -103,20 +164,20 @@ def textify(data, the_user_dict):
# def set_absolute_filename(func):
# # logmessage("Running set_absolute_filename in parse")
-# docassemble.base.functions.set_absolute_filename(func)
+# set_absolute_filename(func)
# def set_url_finder(func):
-# docassemble.base.filter.set_url_finder(func)
-# docassemble.base.functions.set_url_finder(func)
+# set_url_finder(func)
+# set_url_finder(func)
# def set_url_for(func):
-# docassemble.base.filter.set_url_for(func)
+# set_url_for(func)
# def set_file_finder(func):
-# docassemble.base.filter.set_file_finder(func)
+# set_file_finder(func)
# def set_da_send_mail(func):
-# docassemble.base.filter.set_da_send_mail(func)
+# set_da_send_mail(func)
# def blank_save_numbered_file(*args, **kwargs):
# return (None, None, None)
@@ -128,7 +189,7 @@ def textify(data, the_user_dict):
# # logmessage("set the save_numbered_file function to " + str(func))
# save_numbered_file = func
-INITIAL_DICT = {'_internal': {'session_local': {}, 'device_local': {}, 'user_local': {}, 'dirty': {}, 'progress': 0, 'tracker': 0, 'docvar': {}, 'doc_cache': {}, 'steps': 1, 'steps_offset': 0, 'secret': None, 'informed': {}, 'livehelp': {'availability': 'unavailable', 'mode': 'help', 'roles': [], 'partner_roles': []}, 'answered': set(), 'answers': {}, 'objselections': {}, 'starttime': None, 'modtime': None, 'accesstime': {}, 'tasks': {}, 'gather': [], 'event_stack': {}, 'misc': {}}, 'url_args': {}, 'nav': docassemble.base.functions.DANav()}
+INITIAL_DICT = {'_internal': {'session_local': {}, 'device_local': {}, 'user_local': {}, 'dirty': {}, 'progress': 0, 'tracker': 0, 'docvar': {}, 'doc_cache': {}, 'steps': 1, 'steps_offset': 0, 'secret': None, 'informed': {}, 'livehelp': {'availability': 'unavailable', 'mode': 'help', 'roles': [], 'partner_roles': []}, 'answered': set(), 'answers': {}, 'objselections': {}, 'starttime': None, 'modtime': None, 'accesstime': {}, 'tasks': {}, 'gather': [], 'event_stack': {}, 'misc': {}}, 'url_args': {}, 'nav': DANav()}
def set_initial_dict(the_dict):
@@ -140,27 +201,6 @@ def get_initial_dict():
return copy.deepcopy(INITIAL_DICT)
-class DAFileSystemLoader(FileSystemLoader):
-
- def get_source(self, environment, template):
- if ':' not in template:
- return super().get_source(environment, template)
- template_path = None
- for the_filename in question_path_options(template):
- if the_filename is not None:
- template_path = the_filename
- break
- if template_path is None or not os.path.isfile(template_path):
- raise TemplateNotFound(template)
- fspath = os.fspath(os.path.dirname(template_path))
- if fspath not in self.searchpath:
- self.searchpath.append(fspath)
- mtime = os.path.getmtime(template_path)
- with open(template_path, 'r', encoding='utf-8') as fp:
- source = fp.read()
- return source, template_path, lambda: mtime == os.path.getmtime(template_path)
-
-
class PackageImage:
def __init__(self, **kwargs):
@@ -170,261 +210,13 @@ def __init__(self, **kwargs):
self.package = kwargs.get('package', 'docassemble.base')
def get_filename(self):
- return docassemble.base.functions.static_filename_path(str(self.package) + ':' + str(self.filename))
+ return static_filename_path(str(self.package) + ':' + str(self.filename))
def get_reference(self):
# logmessage("get_reference is considering " + str(self.package) + ':' + str(self.filename))
return str(self.package) + ':' + str(self.filename)
-class InterviewSource:
-
- def __init__(self, **kwargs):
- if not hasattr(self, 'package'):
- self.package = kwargs.get('package', None)
- self.language = kwargs.get('language', '*')
- self.dialect = kwargs.get('dialect', None)
- self.testing = kwargs.get('testing', False)
- self.translating = kwargs.get('translating', False)
-
- def __le__(self, other):
- return str(self) <= (str(other) if isinstance(other, InterviewSource) else other)
-
- def __ge__(self, other):
- return str(self) >= (str(other) if isinstance(other, InterviewSource) else other)
-
- def __gt__(self, other):
- return str(self) > (str(other) if isinstance(other, InterviewSource) else other)
-
- def __lt__(self, other):
- return str(self) < (str(other) if isinstance(other, InterviewSource) else other)
-
- def __eq__(self, other):
- return self is other
-
- def __ne__(self, other):
- return self is not other
-
- def __str__(self):
- if hasattr(self, 'path'):
- return str(self.path)
- return 'interviewsource'
-
- def __hash__(self):
- if hasattr(self, 'path'):
- return hash((self.path,))
- return hash(('interviewsource',))
-
- def set_path(self, path):
- self.path = path
-
- def get_name(self):
- if ':' in self.path:
- return self.path
- return self.get_package() + ':data/questions/' + self.path
-
- def get_index(self):
- the_index = docassemble.base.functions.server.server_redis.get('da:interviewsource:' + self.path)
- if the_index is None:
- # logmessage("Updating index from get_index for " + self.path)
- the_index = docassemble.base.functions.server.server_redis.incr('da:interviewsource:' + self.path)
- return the_index
-
- def update_index(self):
- # logmessage("Updating index for " + self.path)
- docassemble.base.functions.server.server_redis.incr('da:interviewsource:' + self.path)
-
- def set_filepath(self, filepath):
- self.filepath = filepath
-
- def set_directory(self, directory):
- self.directory = directory
-
- def set_content(self, content):
- self.content = content
-
- def set_language(self, language):
- self.language = language
-
- def set_dialect(self, dialect):
- self.dialect = dialect
-
- def set_testing(self, testing):
- self.testing = testing
-
- def set_package(self, package):
- self.package = package
-
- def update(self, **kwargs): # pylint: disable=unused-argument
- return True
-
- def get_modtime(self):
- return self._modtime
-
- def get_language(self):
- return self.language
-
- def get_dialect(self):
- return self.dialect
-
- def get_package(self):
- return self.package
-
- def get_testing(self):
- return self.testing
-
- def get_interview(self):
- return Interview(source=self)
-
- def append(self, path): # pylint: disable=unused-argument
- return None
-
-
-class InterviewSourceString(InterviewSource):
-
- def __init__(self, **kwargs):
- self.set_path(kwargs.get('path', None))
- self.set_directory(kwargs.get('directory', None))
- self.set_content(kwargs.get('content', None))
- self._modtime = datetime.datetime.now(tz=datetime.timezone.utc)
- super().__init__(**kwargs)
-
-
-class InterviewSourceFile(InterviewSource):
-
- def __init__(self, **kwargs):
- self.playground = None
- if 'filepath' in kwargs:
- if kwargs['filepath'].__class__.__name__.endswith('SavedFile'):
- self.playground = kwargs['filepath']
- if self.playground.subdir and self.playground.subdir != 'default':
- self.playground_file = os.path.join(self.playground.subdir, self.playground.filename)
- else:
- self.playground_file = self.playground.filename
- # logmessage("The path is " + repr(self.playground.path))
- if os.path.isfile(self.playground.path) and os.access(self.playground.path, os.R_OK):
- self.set_filepath(self.playground.path)
- else:
- logmessage("Details of playground path reference:")
- logmessage("Keyword arguments were " + repr(kwargs))
- for attribute in ['file_number', 'fixed', 'section', 'filename', 'extension', 'directory', 'path', 'modtimes', 'keydict', 'subdir']:
- if hasattr(self.playground, attribute):
- logmessage(attribute + " is " + repr(getattr(self.playground, attribute)))
- else:
- logmessage(attribute + " did not exist")
- if os.path.exists(self.playground.path):
- if os.path.isfile(self.playground.path):
- if os.access(self.playground.path, os.R_OK):
- logmessage("path is a file and is readable")
- else:
- logmessage("path is a file but is not readable")
- else:
- logmessage("path was not a file")
- else:
- logmessage("path did not exist")
- raise DANotFoundError("Reference to invalid playground path.")
- else:
- self.set_filepath(kwargs['filepath'])
- else:
- self.filepath = None
- if 'path' in kwargs:
- self.set_path(kwargs['path'])
- super().__init__(**kwargs)
-
- def set_path(self, path):
- self.path = path
- parts = path.split(":")
- if len(parts) == 2:
- self.package = parts[0]
- self.basename = parts[1]
- else:
- self.package = None
- # if self.package is None:
- # m = re.search(r'^/(playground\.[0-9]+)/', path)
- # if m:
- # self.package = m.group(1)
- if self.filepath is None:
- self.set_filepath(interview_source_from_string(self.path))
- if self.package is None and re.search(r'docassemble.base.data.', self.filepath):
- self.package = 'docassemble.base'
-
- def set_filepath(self, filepath):
- # logmessage("Called set_filepath with " + str(filepath))
- self.filepath = filepath
- if self.filepath is None:
- self.directory = None
- else:
- self.set_directory(os.path.dirname(self.filepath))
-
- def reset_modtime(self):
- try:
- with open(self.filepath, 'a', encoding='utf-8'):
- os.utime(self.filepath, None)
- except:
- logmessage("InterviewSourceFile: could not reset modification time on interview")
-
- def update(self, **kwargs):
- try:
- with open(self.filepath, 'r', encoding='utf-8') as the_file:
- orig_text = the_file.read()
- except:
- return False
- if not orig_text.startswith('# use jinja'):
- self.set_content(orig_text)
- return True
- env = Environment(
- loader=DAFileSystemLoader(self.directory),
- autoescape=select_autoescape()
- )
- if kwargs.get('raise_jinja_errors', True):
- template = env.get_template(os.path.basename(self.filepath))
- else:
- try:
- template = env.get_template(os.path.basename(self.filepath))
- except TemplateError:
- self.set_content(orig_text)
- return True
- data = copy.deepcopy(get_config('jinja data'))
- data['__config__'] = copy.deepcopy(docassemble.base.functions.server.daconfig)
- data['__version__'] = da_version
- data['__architecture__'] = da_arch
- data['__filename__'] = self.path
- data['__current_package__'] = self.package
- data['__parent_filename__'] = kwargs.get('parent_source', self).path
- data['__parent_package__'] = kwargs.get('parent_source', self).package
- data['__interview_filename__'] = kwargs.get('interview_source', self).path
- data['__interview_package__'] = kwargs.get('interview_source', self).package
- data['__hostname__'] = get_config('external hostname', None) or 'localhost'
- data['__debug__'] = bool(get_config('debug', True))
- try:
- self.set_content(template.render(data))
- except BaseException as err:
- self.set_content("__error__: " + repr("Jinja2 rendering error: " + err.__class__.__name__ + ": " + str(err)))
- return True
-
- def get_modtime(self):
- # logmessage("get_modtime called in parse where path is " + str(self.path))
- if self.playground is not None:
- return self.playground.get_modtime(filename=self.playground_file)
- self._modtime = os.path.getmtime(self.filepath)
- return self._modtime
-
- def append(self, path):
- new_file = os.path.join(self.directory, path)
- if os.path.isfile(new_file) and os.access(new_file, os.R_OK):
- new_source = InterviewSourceFile()
- new_source.path = path
- new_source.directory = self.directory
- new_source.basename = path
- new_source.filepath = new_file
- new_source.playground = self.playground
- if hasattr(self, 'package'):
- new_source.package = self.package
- if new_source.update():
- return new_source
- return None
-
-
def dummy_embed_input(status, variable): # pylint: disable=unused-argument
return variable
@@ -781,13 +573,13 @@ def initialize_screen_reader(self):
def populate(self, question_result):
self.question = question_result['question']
- self.questionText = question_result['question_text']
- self.subquestionText = question_result['subquestion_text']
- self.continueLabel = question_result['continue_label']
+ self.question_text = question_result['question_text']
+ self.subquestion_text = question_result['subquestion_text']
+ self.continue_label = question_result['continue_label']
self.decorations = question_result['decorations']
self.audiovideo = question_result['audiovideo']
- self.helpText = question_result['help_text'] or []
- self.interviewHelpText = question_result['interview_help_text'] or []
+ self.help_text = question_result['help_text'] or []
+ self.interview_help_text = question_result['interview_help_text'] or []
self.attachments = question_result['attachments'] or []
self.selectcompute = question_result['selectcompute']
self.defaults = question_result['defaults']
@@ -860,8 +652,8 @@ def get_history(self):
def convert_help(self, help_text, encode, debug):
the_help = {}
if 'audiovideo' in help_text and help_text['audiovideo'] is not None:
- audio_result = docassemble.base.filter.get_audio_urls(help_text['audiovideo'])
- video_result = docassemble.base.filter.get_video_urls(help_text['audiovideo'])
+ audio_result = get_audio_urls(help_text['audiovideo'])
+ video_result = get_video_urls(help_text['audiovideo'])
if len(audio_result) > 0:
the_help['audio'] = [{'url': x[0], 'mime_type': x[1]} for x in audio_result]
if len(video_result) > 0:
@@ -873,12 +665,12 @@ def convert_help(self, help_text, encode, debug):
the_help['help'] = ''
the_help['help'] += '' + the_help['heading'] + '
'
if 'content' in help_text and help_text['content'] is not None:
- the_help['content'] = docassemble.base.filter.markdown_to_html(help_text['content'].rstrip(), status=self, verbatim=(not encode))
+ the_help['content'] = markdown_to_html(help_text['content'].rstrip(), status=self, verbatim=not encode)
if debug:
if 'help' not in the_help:
the_help['help'] = ''
the_help['help'] += '' + the_help['content'] + '
'
- # elif len(self.helpText) > 1:
+ # elif len(self.help_text) > 1:
# the_help['heading'] = word('Help with this question')
return the_help
@@ -895,7 +687,7 @@ def as_data(self, the_user_dict, encode=True):
result['validation_messages'] = {}
if 'reload_after' in self.extras:
result['reload'] = 1000 * int(self.extras['reload_after'])
- lang = docassemble.base.functions.get_language()
+ lang = get_language()
if len(self.question.terms) > 0 or len(self.question.interview.terms) > 0:
result['terms'] = {}
if 'terms' in self.extras:
@@ -924,29 +716,28 @@ def as_data(self, the_user_dict, encode=True):
result['additional_buttons'] = []
for item in self.extras['action_buttons']:
new_item = copy.deepcopy(item)
- new_item['label'] = docassemble.base.filter.markdown_to_html(item['label'], trim=True, do_terms=False, status=self, verbatim=(not encode))
+ new_item['label'] = markdown_to_html(item['label'], trim=True, do_terms=False, status=self, verbatim=not encode)
if debug:
output['question'] += '' + new_item['label'] + '
'
- for param in ('questionText',):
- if hasattr(self, param) and getattr(self, param) is not None:
- result[param] = docassemble.base.filter.markdown_to_html(getattr(self, param).rstrip(), trim=True, status=self, verbatim=(not encode))
- if debug:
- output['question'] += '' + result[param] + '
'
+ if hasattr(self, 'question_text') and getattr(self, 'question_text') is not None:
+ result['questionText'] = markdown_to_html(getattr(self, 'question_text').rstrip(), trim=True, status=self, verbatim=not encode)
+ if debug:
+ output['question'] += '' + result['questionText'] + '
'
if debug:
if hasattr(self, 'breadcrumb') and self.breadcrumb is not None:
output['breadcrumb label'] = self.breadcrumb
- output['breadcrumbs'] = docassemble.base.functions.get_action_stack()
- if hasattr(self, 'subquestionText') and self.subquestionText is not None:
+ output['breadcrumbs'] = get_action_stack()
+ if hasattr(self, 'subquestion_text') and self.subquestion_text is not None:
if self.question.question_type == "fields":
embedder = dummy_embed_input
else:
embedder = None
- result['subquestionText'] = docassemble.base.filter.markdown_to_html(self.subquestionText.rstrip(), status=self, verbatim=(not encode), embedder=embedder)
+ result['subquestionText'] = markdown_to_html(self.subquestion_text.rstrip(), status=self, verbatim=not encode, embedder=embedder)
if debug:
output['question'] += result['subquestionText']
- for param in ('continueLabel', 'helpLabel'):
- if hasattr(self, param) and getattr(self, param) is not None:
- result[param] = docassemble.base.filter.markdown_to_html(getattr(self, param).rstrip(), trim=True, do_terms=False, status=self, verbatim=(not encode))
+ for attr, param in (('continue_label', 'continueLabel'), ('help_label', 'helpLabel')):
+ if hasattr(self, attr) and getattr(self, attr) is not None:
+ result[param] = markdown_to_html(getattr(self, attr).rstrip(), trim=True, do_terms=False, status=self, verbatim=not encode)
if debug:
output['question'] += '' + result[param] + '
'
if 'menu_items' in self.extras and isinstance(self.extras['menu_items'], list):
@@ -956,10 +747,10 @@ def as_data(self, the_user_dict, encode=True):
result[param] = self.extras[param].rstrip()
for param in ('back_button_label',):
if param in self.extras and isinstance(self.extras[param], str):
- result[param] = docassemble.base.filter.markdown_to_html(self.extras[param].rstrip(), trim=True, do_terms=False, status=self, verbatim=(not encode))
+ result[param] = markdown_to_html(self.extras[param].rstrip(), trim=True, do_terms=False, status=self, verbatim=not encode)
for param in ('rightText', 'underText'):
if param in self.extras and isinstance(self.extras[param], str):
- result[param] = docassemble.base.filter.markdown_to_html(self.extras[param].rstrip(), status=self, verbatim=(not encode))
+ result[param] = markdown_to_html(self.extras[param].rstrip(), status=self, verbatim=not encode)
if debug:
output['question'] += result[param]
if 'continueLabel' not in result:
@@ -1007,20 +798,20 @@ def as_data(self, the_user_dict, encode=True):
if hasattr(self.question, 'id'):
result['id'] = self.question.id
if hasattr(self, 'audiovideo') and self.audiovideo is not None:
- audio_result = docassemble.base.filter.get_audio_urls(self.audiovideo)
- video_result = docassemble.base.filter.get_video_urls(self.audiovideo)
+ audio_result = get_audio_urls(self.audiovideo)
+ video_result = get_video_urls(self.audiovideo)
if len(audio_result) > 0:
result['audio'] = [{'url': re.sub(r'.*"(http[^"]+)".*', r'\1', x)} if isinstance(x, str) else {'url': x[0], 'mime_type': x[1]} for x in audio_result]
if len(video_result) > 0:
result['video'] = [{'url': re.sub(r'.*"(http[^"]+)".*', r'\1', x)} if isinstance(x, str) else {'url': x[0], 'mime_type': x[1]} for x in video_result]
- if hasattr(self, 'helpText') and len(self.helpText) > 0:
+ if hasattr(self, 'help_text') and len(self.help_text) > 0:
result['helpText'] = []
result['helpBackLabel'] = word("Back to question")
- for help_text in self.helpText:
+ for help_text in self.help_text:
result['helpText'].append(self.convert_help(help_text, encode, debug))
result['help'] = {}
- if self.helpText[0]['label']:
- result['help']['label'] = docassemble.base.filter.markdown_to_html(self.helpText[0]['label'], trim=True, do_terms=False, status=self, verbatim=(not encode))
+ if self.help_text[0]['label']:
+ result['help']['label'] = markdown_to_html(self.help_text[0]['label'], trim=True, do_terms=False, status=self, verbatim=not encode)
else:
result['help']['label'] = self.question.help()
result['help']['title'] = word("Help is available for this question")
@@ -1033,9 +824,9 @@ def as_data(self, the_user_dict, encode=True):
if 'help' in item:
output['help'] += '' + item['help'] + ''
output['help'] += ''
- if hasattr(self, 'interviewHelpText') and len(self.interviewHelpText) > 0:
+ if hasattr(self, 'interview_help_text') and len(self.interview_help_text) > 0:
result['interviewHelpText'] = []
- for help_text in self.interviewHelpText:
+ for help_text in self.interview_help_text:
result['interviewHelpText'].append(self.convert_help(help_text, encode, debug))
if debug:
for item in result['interviewHelpText']:
@@ -1043,12 +834,12 @@ def as_data(self, the_user_dict, encode=True):
output['help'] += '' + item['help'] + ''
if 'help' not in result:
result['help'] = {}
- if self.interviewHelpText[0]['label']:
- result['help']['interviewLabel'] = docassemble.base.filter.markdown_to_html(self.interviewHelpText[0]['label'], trim=True, do_terms=False, status=self, verbatim=(not encode))
+ if self.interview_help_text[0]['label']:
+ result['help']['interviewLabel'] = markdown_to_html(self.interview_help_text[0]['label'], trim=True, do_terms=False, status=self, verbatim=not encode)
else:
result['help']['interviewLabel'] = self.question.help()
result['help']['interviewTitle'] = word("Help is available")
- if not (hasattr(self, 'helpText') and len(self.helpText) > 0):
+ if not (hasattr(self, 'help_text') and len(self.help_text) > 0):
result['help']['specific'] = False
if 'questionText' not in result and self.question.question_type == "signature":
result['questionText'] = '' + word('Sign Your Name') + '
'
@@ -1080,9 +871,9 @@ def as_data(self, the_user_dict, encode=True):
result['decoration'] = {}
the_image = self.question.interview.images.get(decoration['image'], None)
if the_image is not None:
- the_url = docassemble.base.functions.server.url_finder(str(the_image.package) + ':' + str(the_image.filename))
+ the_url = url_finder(str(the_image.package) + ':' + str(the_image.filename))
width = str(width_value) + str(width_units)
- filename = docassemble.base.functions.server.file_finder(str(the_image.package) + ':' + str(the_image.filename))
+ filename = file_finder(str(the_image.package) + ':' + str(the_image.filename))
if 'extension' in filename and filename['extension'] == 'svg' and 'width' in filename:
if filename['width'] and filename['height']:
height = str(width_value * (filename['height']/filename['width'])) + str(width_units)
@@ -1110,19 +901,19 @@ def as_data(self, the_user_dict, encode=True):
the_attachment['variable_name'] = attachment['orig_variable_name']
if 'name' in attachment:
if attachment['name']:
- the_attachment['name'] = docassemble.base.filter.markdown_to_html(attachment['name'], trim=True, status=self, verbatim=(not encode))
+ the_attachment['name'] = markdown_to_html(attachment['name'], trim=True, status=self, verbatim=not encode)
if debug:
output['question'] += '' + the_attachment['name'] + '
'
if 'description' in attachment:
if attachment['description']:
- the_attachment['description'] = docassemble.base.filter.markdown_to_html(attachment['description'], status=self, verbatim=(not encode))
+ the_attachment['description'] = markdown_to_html(attachment['description'], status=self, verbatim=not encode)
if debug:
output['question'] += '' + the_attachment['description'] + '
'
for key in ('valid_formats', 'filename', 'content', 'markdown', 'raw'):
if key in attachment and attachment[key]:
the_attachment[key] = attachment[key]
for the_format in attachment['file']:
- the_attachment['url'][the_format] = docassemble.base.functions.server.url_finder(attachment['file'][the_format], filename=attachment['filename'] + '.' + extension_of_doc_format.get(the_format, the_format))
+ the_attachment['url'][the_format] = url_finder(attachment['file'][the_format], filename=attachment['filename'] + '.' + extension_of_doc_format.get(the_format, the_format))
the_attachment['number'][the_format] = attachment['file'][the_format]
the_attachment['filename_with_extension'][the_format] = attachment['filename'] + '.' + extension_of_doc_format.get(the_format, the_format)
result['attachments'].append(the_attachment)
@@ -1210,26 +1001,26 @@ def as_data(self, the_user_dict, encode=True):
if field.datatype == 'date':
the_field['validation_messages']['date'] = field.validation_message('date', self, word("You need to enter a valid date."))
if hasattr(field, 'extras') and 'min' in field.extras and 'min' in self.extras and 'max' in field.extras and 'max' in self.extras and field.number in self.extras['min'] and field.number in self.extras['max']:
- the_field['validation_messages']['minmax'] = field.validation_message('date minmax', self, word("You need to enter a date between %s and %s."), parameters=(docassemble.base.util.format_date(self.extras['min'][field.number], format='medium'), docassemble.base.util.format_date(self.extras['max'][field.number], format='medium')))
+ the_field['validation_messages']['minmax'] = field.validation_message('date minmax', self, word("You need to enter a date between %s and %s."), parameters=(format_date(self.extras['min'][field.number], format='medium'), format_date(self.extras['max'][field.number], format='medium')))
else:
was_defined = {}
for key in ['min', 'max']:
if hasattr(field, 'extras') and key in field.extras and key in self.extras and field.number in self.extras[key]:
was_defined[key] = True
if key == 'min':
- the_field['validation_messages']['min'] = field.validation_message('date min', self, word("You need to enter a date on or after %s."), parameters=tuple([docassemble.base.util.format_date(self.extras[key][field.number], format='medium')]))
+ the_field['validation_messages']['min'] = field.validation_message('date min', self, word("You need to enter a date on or after %s."), parameters=tuple([format_date(self.extras[key][field.number], format='medium')]))
elif key == 'max':
- the_field['validation_messages']['max'] = field.validation_message('date max', self, word("You need to enter a date on or before %s."), parameters=tuple([docassemble.base.util.format_date(self.extras[key][field.number], format='medium')]))
+ the_field['validation_messages']['max'] = field.validation_message('date max', self, word("You need to enter a date on or before %s."), parameters=tuple([format_date(self.extras[key][field.number], format='medium')]))
if len(was_defined) == 0 and 'default date min' in self.question.interview.options and 'default date max' in self.question.interview.options:
- the_field['min'] = docassemble.base.util.format_date(self.question.interview.options['default date min'], format='yyyy-MM-dd')
- the_field['max'] = docassemble.base.util.format_date(self.question.interview.options['default date max'], format='yyyy-MM-dd')
- the_field['validation_messages']['minmax'] = field.validation_message('date minmax', self, word("You need to enter a date between %s and %s."), parameters=(docassemble.base.util.format_date(self.question.interview.options['default date min'], format='medium'), docassemble.base.util.format_date(self.question.interview.options['default date max'], format='medium')))
+ the_field['min'] = format_date(self.question.interview.options['default date min'], format='yyyy-MM-dd')
+ the_field['max'] = format_date(self.question.interview.options['default date max'], format='yyyy-MM-dd')
+ the_field['validation_messages']['minmax'] = field.validation_message('date minmax', self, word("You need to enter a date between %s and %s."), parameters=(format_date(self.question.interview.options['default date min'], format='medium'), format_date(self.question.interview.options['default date max'], format='medium')))
elif 'max' not in was_defined and 'default date max' in self.question.interview.options:
- the_field['max'] = docassemble.base.util.format_date(self.question.interview.options['default date max'], format='yyyy-MM-dd')
- the_field['validation_messages']['max'] = field.validation_message('date max', self, word("You need to enter a date on or before %s."), parameters=tuple([docassemble.base.util.format_date(self.question.interview.options['default date max'], format='medium')]))
+ the_field['max'] = format_date(self.question.interview.options['default date max'], format='yyyy-MM-dd')
+ the_field['validation_messages']['max'] = field.validation_message('date max', self, word("You need to enter a date on or before %s."), parameters=tuple([format_date(self.question.interview.options['default date max'], format='medium')]))
elif 'min' not in was_defined and 'default date min' in self.question.interview.options:
- the_field['min'] = docassemble.base.util.format_date(self.question.interview.options['default date min'], format='yyyy-MM-dd')
- the_field['validation_messages']['min'] = field.validation_message('date min', self, word("You need to enter a date on or after %s."), parameters=tuple([docassemble.base.util.format_date(self.question.interview.options['default date min'], format='medium')]))
+ the_field['min'] = format_date(self.question.interview.options['default date min'], format='yyyy-MM-dd')
+ the_field['validation_messages']['min'] = field.validation_message('date min', self, word("You need to enter a date on or after %s."), parameters=tuple([format_date(self.question.interview.options['default date min'], format='medium')]))
if field.datatype == 'time':
the_field['validation_messages']['time'] = field.validation_message('time', self, word("You need to enter a valid time."))
if field.datatype in ['datetime', 'datetime-local']:
@@ -1296,9 +1087,9 @@ def as_data(self, the_user_dict, encode=True):
if 'label' in item:
output['question'] += '' + item['label'] + '
'
if hasattr(field, 'aota'):
- the_field['all_of_the_above'] = docassemble.base.filter.markdown_to_html(self.extras['aota'][field.number], do_terms=False, status=self, verbatim=(not encode))
+ the_field['all_of_the_above'] = markdown_to_html(self.extras['aota'][field.number], do_terms=False, status=self, verbatim=not encode)
if hasattr(field, 'nota'):
- the_field['none_of_the_above'] = docassemble.base.filter.markdown_to_html(self.extras['nota'][field.number], do_terms=False, status=self, verbatim=(not encode))
+ the_field['none_of_the_above'] = markdown_to_html(self.extras['nota'][field.number], do_terms=False, status=self, verbatim=not encode)
if field.number in self.extras['ok']:
the_field['active'] = self.extras['ok'][field.number]
else:
@@ -1335,7 +1126,7 @@ def as_data(self, the_user_dict, encode=True):
if the_expression:
the_field['show_if_js'] = {'expression': the_expression, 'vars': field.extras['show_if_js']['vars'], 'sign': field.extras['show_if_js']['sign'], 'mode': field.extras['show_if_js']['mode']}
if 'note' in self.extras and field.number in self.extras['note']:
- the_field['note'] = docassemble.base.filter.markdown_to_html(self.extras['note'][field.number], status=self, verbatim=(not encode))
+ the_field['note'] = markdown_to_html(self.extras['note'][field.number], status=self, verbatim=not encode)
if 'html' in self.extras and field.number in self.extras['html']:
the_field['html'] = self.extras['html'][field.number]
if 'raw html' in self.extras and field.number in self.extras['raw html']:
@@ -1345,21 +1136,21 @@ def as_data(self, the_user_dict, encode=True):
if debug:
output['question'] += '' + the_field['hint'] + '
'
if field.number in self.labels:
- the_field['label'] = docassemble.base.filter.markdown_to_html(self.labels[field.number], trim=True, status=self, verbatim=(not encode))
+ the_field['label'] = markdown_to_html(self.labels[field.number], trim=True, status=self, verbatim=not encode)
if debug:
output['question'] += '' + the_field['label'] + '
'
if field.number in self.helptexts:
- the_field['helptext'] = docassemble.base.filter.markdown_to_html(self.helptexts[field.number], status=self, verbatim=(not encode))
+ the_field['helptext'] = markdown_to_html(self.helptexts[field.number], status=self, verbatim=not encode)
if debug:
output['question'] += '' + the_field['helptext'] + '
'
if self.question.question_type in ("yesno", "yesnomaybe"):
- the_field['true_label'] = docassemble.base.filter.markdown_to_html(self.question.yes(), trim=True, do_terms=False, status=self, verbatim=(not encode))
- the_field['false_label'] = docassemble.base.filter.markdown_to_html(self.question.no(), trim=True, do_terms=False, status=self, verbatim=(not encode))
+ the_field['true_label'] = markdown_to_html(self.question.yes(), trim=True, do_terms=False, status=self, verbatim=not encode)
+ the_field['false_label'] = markdown_to_html(self.question.no(), trim=True, do_terms=False, status=self, verbatim=not encode)
if debug:
output['question'] += '' + the_field['true_label'] + '
'
output['question'] += '' + the_field['false_label'] + '
'
if self.question.question_type == 'yesnomaybe':
- the_field['maybe_label'] = docassemble.base.filter.markdown_to_html(self.question.maybe(), trim=True, do_terms=False, status=self, verbatim=(not encode))
+ the_field['maybe_label'] = markdown_to_html(self.question.maybe(), trim=True, do_terms=False, status=self, verbatim=not encode)
if debug:
output['question'] += '' + the_field['maybe_label'] + '
'
result['fields'].append(the_field)
@@ -1381,7 +1172,7 @@ def as_data(self, the_user_dict, encode=True):
for question_type in ('question', 'help'):
if question_type not in output:
continue
- phrase = docassemble.base.functions.server.to_text('' + output[question_type] + '')
+ phrase = to_text('' + output[question_type] + '')
if (not phrase) or len(phrase) < 10:
phrase = "The sky is blue."
phrase = re.sub(r'[^A-Za-z 0-9\.\,\?\#\!\%\&\(\)]', r' ', phrase)
@@ -1449,7 +1240,7 @@ def icon_url(self, name):
return None
if the_image.attribution is not None:
self.attributions.add(the_image.attribution)
- url = docassemble.base.functions.server.url_finder(str(the_image.package) + ':' + str(the_image.filename))
+ url = url_finder(str(the_image.package) + ':' + str(the_image.filename))
return url
def get_choices_data(self, field, defaultvalue, the_user_dict, encode=True):
@@ -1460,9 +1251,9 @@ def get_choices_data(self, field, defaultvalue, the_user_dict, encode=True):
if self.question.question_type == "multiple_choice":
pairlist = list(self.selectcompute[field.number])
for pair in pairlist:
- item = {'label': docassemble.base.filter.markdown_to_html(pair['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'value': pair['key']}
+ item = {'label': markdown_to_html(pair['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'value': pair['key']}
if 'help' in pair:
- item['help'] = docassemble.base.filter.markdown_to_html(pair['help'].rstrip(), trim=True, do_terms=False, status=self, verbatim=encode)
+ item['help'] = markdown_to_html(pair['help'].rstrip(), trim=True, do_terms=False, status=self, verbatim=encode)
for standard_key in ('default', 'css class', 'color', 'group'):
if standard_key in pair:
item[standard_key] = pair[standard_key]
@@ -1486,7 +1277,7 @@ def get_choices_data(self, field, defaultvalue, the_user_dict, encode=True):
pairlist = []
if field.datatype in ('object_multiselect', 'object_checkboxes'):
for pair in pairlist:
- item = {'label': docassemble.base.filter.markdown_to_html(pair['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'value': from_safeid(pair['key'])}
+ item = {'label': markdown_to_html(pair['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'value': from_safeid(pair['key'])}
if ('default' in pair and pair['default']) or (defaultvalue is not None and isinstance(defaultvalue, (list, set)) and str(pair['key']) in defaultvalue) or (isinstance(defaultvalue, dict) and str(pair['key']) in defaultvalue and defaultvalue[str(pair['key'])]) or (isinstance(defaultvalue, (str, int, bool, float)) and str(pair['key']) == str(defaultvalue)):
item['selected'] = True
for standard_key in ('help', 'css class', 'color'):
@@ -1499,7 +1290,7 @@ def get_choices_data(self, field, defaultvalue, the_user_dict, encode=True):
choice_list.append(item)
elif field.datatype in ('object', 'object_radio'):
for pair in pairlist:
- item = {'label': docassemble.base.filter.markdown_to_html(pair['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'value': from_safeid(pair['key'])}
+ item = {'label': markdown_to_html(pair['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'value': from_safeid(pair['key'])}
if ('default' in pair and pair['default']) or (defaultvalue is not None and isinstance(defaultvalue, (str, int, bool, float)) and str(pair['key']) == str(defaultvalue)):
item['selected'] = True
if 'default' in pair:
@@ -1514,7 +1305,7 @@ def get_choices_data(self, field, defaultvalue, the_user_dict, encode=True):
choice_list.append(item)
elif field.datatype in ('multiselect', 'checkboxes'):
for pair in pairlist:
- item = {'label': docassemble.base.filter.markdown_to_html(pair['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'variable_name': saveas + "[" + repr(pair['key']) + "]", 'value': True}
+ item = {'label': markdown_to_html(pair['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'variable_name': saveas + "[" + repr(pair['key']) + "]", 'value': True}
if encode:
item['variable_name_encoded'] = safeid(saveas + "[" + repr(pair['key']) + "]")
if ('default' in pair and pair['default']) or (defaultvalue is not None and isinstance(defaultvalue, (list, set)) and str(pair['key']) in defaultvalue) or (isinstance(defaultvalue, dict) and str(pair['key']) in defaultvalue and defaultvalue[str(pair['key'])]) or (isinstance(defaultvalue, (str, int, bool, float)) and str(pair['key']) == str(defaultvalue)):
@@ -1529,7 +1320,7 @@ def get_choices_data(self, field, defaultvalue, the_user_dict, encode=True):
choice_list.append(item)
else:
for pair in pairlist:
- item = {'label': docassemble.base.filter.markdown_to_html(pair['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'value': pair['key']}
+ item = {'label': markdown_to_html(pair['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'value': pair['key']}
if ('default' in pair and pair['default']) or (defaultvalue is not None and isinstance(defaultvalue, (str, int, bool, float)) and str(pair['key']) == str(defaultvalue)):
item['selected'] = True
choice_list.append(item)
@@ -1538,17 +1329,17 @@ def get_choices_data(self, field, defaultvalue, the_user_dict, encode=True):
formatted_item = word("All of the above")
else:
formatted_item = self.extras['aota'][field.number]
- choice_list.append({'label': docassemble.base.filter.markdown_to_html(formatted_item, trim=True, do_terms=False, status=self, verbatim=encode)})
+ choice_list.append({'label': markdown_to_html(formatted_item, trim=True, do_terms=False, status=self, verbatim=encode)})
if hasattr(field, 'nota') and self.extras['nota'][field.number] is not False:
if self.extras['nota'][field.number] is True:
formatted_item = word("None of the above")
else:
formatted_item = self.extras['nota'][field.number]
- choice_list.append({'label': docassemble.base.filter.markdown_to_html(formatted_item, trim=True, do_terms=False, status=self, verbatim=encode)})
+ choice_list.append({'label': markdown_to_html(formatted_item, trim=True, do_terms=False, status=self, verbatim=encode)})
else:
indexno = 0
for choice in self.selectcompute[field.number]:
- item = {'label': docassemble.base.filter.markdown_to_html(choice['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'variable_name': '_internal["answers"][' + repr(question.extended_question_name(the_user_dict)) + ']', 'value': indexno}
+ item = {'label': markdown_to_html(choice['label'], trim=True, do_terms=False, status=self, verbatim=encode), 'variable_name': '_internal["answers"][' + repr(question.extended_question_name(the_user_dict)) + ']', 'value': indexno}
if encode:
item['variable_name_encoded'] = safeid('_internal["answers"][' + repr(question.extended_question_name(the_user_dict)) + ']')
if 'image' in choice:
@@ -1629,7 +1420,7 @@ def __init__(self, x, question=None, translate=True):
def text(self, the_user_dict):
if len(self.other_lang) > 0:
- target_lang = docassemble.base.functions.get_language()
+ target_lang = get_language()
if self.language != target_lang and target_lang in self.other_lang:
if self.uses_mako:
return self.other_lang[target_lang][1].render(**the_user_dict)
@@ -1869,7 +1660,7 @@ def recursive_eval_textobject(target, the_user_dict, question, tpl, skip_undefin
text = ''
else:
text = target.text(the_user_dict)
- return docassemble.base.file_docx.transform_for_docx(text)
+ return transform_for_docx(text)
raise DAError("recursive_eval_textobject: expected a TextObject, but found a " + str(type(target)))
@@ -1917,25 +1708,6 @@ def recursive_eval_textobject_or_primitive(target, the_user_dict):
raise DAError("recursive_eval_textobject_or_primitive: expected a TextObject, but found a " + str(type(target)))
-def fix_quotes(match):
- instring = match.group(1)
- n = len(instring)
- output = ''
- i = 0
- while i < n:
- if instring[i] == '\u201c' or instring[i] == '\u201d':
- output += '"'
- elif instring[i] == '\u2018' or instring[i] == '\u2019':
- output += "'"
- elif instring[i] == '&' and i + 4 < n and instring[i:i+5] == '&':
- output += '&'
- i += 4
- else:
- output += instring[i]
- i += 1
- return output
-
-
def docx_variable_fix(variable):
variable = re.sub(r'\\', '', variable)
variable = re.sub(r'^([A-Za-z\_][A-Za-z\_0-9]*).*', r'\1', variable)
@@ -1950,7 +1722,7 @@ class FileInPackage:
def __init__(self, fileref, area, package):
if area == 'template' and not isinstance(fileref, dict):
- docassemble.base.functions.package_template_filename(fileref, package=package)
+ package_template_filename(fileref, package=package)
self.fileref = fileref
if isinstance(self.fileref, dict):
self.is_code = True
@@ -1991,9 +1763,9 @@ def path(self, the_user_dict=None):
raise DAError("FileInPackage: error downloading " + str(the_file_ref) + ": " + str(err))
the_file_ref = temp_template_file.name
if not str(the_file_ref).startswith('/'):
- the_file_ref = docassemble.base.functions.package_template_filename(str(the_file_ref), package=self.package)
+ the_file_ref = package_template_filename(str(the_file_ref), package=self.package)
return the_file_ref
- return docassemble.base.functions.package_template_filename(self.fileref, package=self.package)
+ return package_template_filename(self.fileref, package=self.package)
return None
def paths(self, the_user_dict=None):
@@ -2025,11 +1797,11 @@ def paths(self, the_user_dict=None):
else:
result.append(the_file_ref)
else:
- result.append(docassemble.base.functions.package_template_filename(self.fileref, package=self.package))
+ result.append(package_template_filename(self.fileref, package=self.package))
final_result = []
for the_file_ref in result:
if not str(the_file_ref).startswith('/'):
- final_result.append(docassemble.base.functions.package_template_filename(str(the_file_ref), package=self.package))
+ final_result.append(package_template_filename(str(the_file_ref), package=self.package))
else:
final_result.append(the_file_ref)
return final_result
@@ -2043,7 +1815,7 @@ def __init__(self, fileref, question):
self.question = question
def path(self):
- info = docassemble.base.functions.server.file_finder(self.fileref, question=self.question)
+ info = file_finder(self.fileref, question=self.question)
if 'fullpath' in info and info['fullpath']:
return info['fullpath']
raise DAError("Could not find the file " + str(self.fileref))
@@ -2171,7 +1943,7 @@ def __init__(self, orig_data, caller, **kwargs):
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', 'describe file types', 'manual attachment list', 'breadcrumb', 'tabular', 'hide continue button', 'disable continue button', 'pen color', 'gathered', 'sort key', 'filter'):
+ 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', 'describe file types', 'manual attachment list', 'breadcrumb', 'tabular', 'hide continue button', 'disable continue button', 'pen color', 'gathered', 'sort key', 'sort reverse', 'filter', 'flattened checkbox label', 'flattened checkbox unselected label'):
logmessage("Ignoring unknown dictionary key '" + key + "'." + self.idebug(data))
if 'features' in data:
should_append = False
@@ -2262,7 +2034,7 @@ def __init__(self, orig_data, caller, **kwargs):
data['features']['custom datatypes to load'] = [data['features']['custom datatypes to load']]
if isinstance(data['features']['custom datatypes to load'], list):
for item in data['features']['custom datatypes to load']:
- if isinstance(item, str) and item not in standard_types and item in docassemble.base.functions.custom_types:
+ if isinstance(item, str) and item not in standard_types and item in custom_types:
self.interview.custom_data_types.add(item)
if 'checkin interval' in data['features']:
if not isinstance(data['features']['checkin interval'], int):
@@ -2302,7 +2074,7 @@ def __init__(self, orig_data, caller, **kwargs):
if not isinstance(data['features'][key], str):
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()))
+ self.interview.options[key] = dateutil.parser.parse(data['features'][key]).astimezone(zoneinfo.ZoneInfo(get_default_timezone()))
except:
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):
@@ -2351,7 +2123,7 @@ def __init__(self, orig_data, caller, **kwargs):
for item in tr_todo:
self.interview.translations.append(item)
if item.endswith(".xlsx"):
- the_xlsx_file = docassemble.base.functions.package_data_filename(item)
+ the_xlsx_file = package_data_filename(item)
if not os.path.isfile(the_xlsx_file):
raise DAError("The translations file " + the_xlsx_file + " could not be found")
import pandas # pylint: disable=import-outside-toplevel
@@ -2368,7 +2140,7 @@ def __init__(self, orig_data, caller, **kwargs):
self.interview.translation_dict[df['orig_text'][indexno]][df['orig_lang'][indexno]] = {}
self.interview.translation_dict[df['orig_text'][indexno]][df['orig_lang'][indexno]][df['tr_lang'][indexno]] = df['tr_text'][indexno]
elif item.endswith(".xlf") or item.endswith(".xliff"):
- the_xlf_file = docassemble.base.functions.package_data_filename(item)
+ the_xlf_file = package_data_filename(item)
if not os.path.isfile(the_xlf_file):
continue
tree = ET.parse(the_xlf_file)
@@ -3158,19 +2930,19 @@ def __init__(self, orig_data, caller, **kwargs):
if isinstance(data['include'], str):
data['include'] = [data['include']]
if isinstance(data['include'], list):
- for questionPath in data['include']:
+ for question_path in data['include']:
try:
- if ':' in questionPath:
- self.interview.read_from(interview_source_from_string(questionPath, interview_source=self.interview.source, parent_source=self.from_source))
+ if ':' in question_path:
+ self.interview.read_from(interview_source_from_string(question_path, interview_source=self.interview.source, parent_source=self.from_source))
else:
- new_source = self.from_source.append(questionPath)
+ new_source = self.from_source.append(question_path)
if new_source is None:
- new_source = interview_source_from_string('docassemble.base:data/questions/' + re.sub(r'^data/questions/', '', questionPath), interview_source=self.interview.source, parent_source=self.from_source)
+ new_source = interview_source_from_string('docassemble.base:data/questions/' + re.sub(r'^data/questions/', '', question_path), interview_source=self.interview.source, parent_source=self.from_source)
if new_source is None:
- raise DANotFoundError('Question file ' + questionPath + ' not found')
+ raise DANotFoundError('Question file ' + question_path + ' not found')
self.interview.read_from(new_source)
except DANotFoundError:
- raise DASourceError('An include section could not find the file ' + str(questionPath) + '.' + self.idebug(data))
+ raise DASourceError('An include section could not find the file ' + str(question_path) + '.' + self.idebug(data))
else:
raise DASourceError("An include section must be organized as a list." + self.idebug(data))
if 'action buttons' in data:
@@ -3365,7 +3137,7 @@ def __init__(self, orig_data, caller, **kwargs):
if hasattr(data['response filename'], 'mimetype') and data['response filename'].mimetype:
self.content_type = TextObject(data['response filename'].mimetype)
else:
- info = docassemble.base.functions.server.file_finder(data['response filename'], question=self)
+ info = file_finder(data['response filename'], question=self)
if 'fullpath' in info and info['fullpath']:
self.response_file = FileOnServer(data['response filename'], self) # info['fullpath']
else:
@@ -3642,7 +3414,7 @@ def __init__(self, orig_data, caller, **kwargs):
field_data['saveas'] = data['field']
if 'datatype' in data and 'type' not in field_data:
field_data['type'] = data['datatype']
- if data['datatype'] not in standard_types and data['datatype'] in docassemble.base.functions.custom_types:
+ if data['datatype'] not in standard_types and data['datatype'] in custom_types:
self.interview.custom_data_types.add(data['datatype'])
elif is_boolean(field_data):
field_data['type'] = 'boolean'
@@ -3879,7 +3651,7 @@ def __init__(self, orig_data, caller, **kwargs):
for content_file in data['content file']:
if not isinstance(content_file, str):
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)
+ file_to_read = package_template_filename(content_file, package=self.package)
# if file_to_read is not None and get_mimetype(file_to_read) != 'text/markdown':
# 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):
@@ -3976,7 +3748,7 @@ def __init__(self, orig_data, caller, **kwargs):
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
+ this_thread.misc['current_field'] = field_number
if not isinstance(field, dict):
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()
@@ -4008,7 +3780,7 @@ def __init__(self, orig_data, caller, **kwargs):
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 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:
+ if field['datatype'] in 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:
@@ -4027,8 +3799,8 @@ def __init__(self, orig_data, caller, **kwargs):
field_info['extras'] = {'fields_code': compile(field['code'], '', 'eval')}
self.fields.append(Field(field_info))
field_number += 1
- if 'current_field' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['current_field']
+ if 'current_field' in this_thread.misc:
+ del this_thread.misc['current_field']
continue
if 'object labeler' in field and ('datatype' not in field or not field['datatype'].startswith('object')):
raise DASourceError("An object labeler can only be used with an object data type." + self.idebug(data))
@@ -4038,14 +3810,14 @@ def __init__(self, orig_data, caller, **kwargs):
if key == 'default' and 'datatype' in field and field['datatype'] in ('object', 'object_radio', 'object_multiselect', 'object_checkboxes'):
continue
if custom_data_type:
- if key in docassemble.base.functions.custom_types[field['datatype']]['parameters']:
+ if key in custom_types[field['datatype']]['parameters']:
if 'extras' not in field_info:
field_info['extras'] = {}
if 'custom_parameters' not in field_info['extras']:
field_info['extras']['custom_parameters'] = {}
field_info['extras']['custom_parameters'][key] = field[key]
continue
- if key in docassemble.base.functions.custom_types[field['datatype']]['code_parameters']:
+ if key in custom_types[field['datatype']]['code_parameters']:
if 'extras' not in field_info:
field_info['extras'] = {}
if 'custom_parameters_code' not in field_info['extras']:
@@ -4053,7 +3825,7 @@ def __init__(self, orig_data, caller, **kwargs):
field_info['extras']['custom_parameters_code'][key] = {'compute': compile(str(field[key]), '', 'eval'), 'sourcecode': str(field[key])}
self.find_fields_in(field[key])
continue
- if key in docassemble.base.functions.custom_types[field['datatype']]['mako_parameters']:
+ if key in custom_types[field['datatype']]['mako_parameters']:
if 'extras' not in field_info:
field_info['extras'] = {}
if 'custom_parameters_mako' not in field_info['extras']:
@@ -4648,8 +4420,8 @@ def __init__(self, orig_data, caller, **kwargs):
else:
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 'current_field' in this_thread.misc:
+ del this_thread.misc['current_field']
if 'review' in data:
self.question_type = 'review'
if self.is_mandatory and 'continue button field' not in data:
@@ -5016,18 +4788,18 @@ def exec_setup(self, is_generic, the_x, iterators, the_user_dict):
for indexno, item in enumerate(iterators):
exec(list_of_indices[indexno] + " = " + item, the_user_dict)
for the_field in [substitute_vars(item, is_generic, the_x, iterators) for item in self.undefine]:
- docassemble.base.functions.undefine(the_field)
+ undefine(the_field)
if len(self.reconsider) > 0:
- docassemble.base.functions.reconsider(*[substitute_vars(item, is_generic, the_x, iterators) for item in self.reconsider])
+ reconsider(*[substitute_vars(item, is_generic, the_x, iterators) for item in self.reconsider])
if self.need is not None:
for need_code in self.need:
eval(need_code, the_user_dict)
def exec_setup_mandatory(self, the_user_dict):
for the_field in self.undefine:
- docassemble.base.functions.undefine(the_field)
+ undefine(the_field)
if len(self.reconsider) > 0:
- docassemble.base.functions.reconsider(*self.reconsider)
+ reconsider(*self.reconsider)
if self.need is not None:
for need_code in self.need:
eval(need_code, the_user_dict)
@@ -5075,7 +4847,7 @@ def recursive_dataobject(self, target):
return TextObject(str(target), question=self)
def find_fields_in(self, code):
- myvisitor = myvisitnode()
+ myvisitor = MyVisitNode()
t = ast.parse(str(code))
myvisitor.visit(t)
predefines = set(globals().keys()) | set(locals().keys())
@@ -5150,7 +4922,7 @@ def process_attachment(self, orig_target):
if 'name' not in target:
target['name'] = word("Document")
if 'filename' not in target:
- # target['filename'] = docassemble.base.functions.space_to_underscore(target['name'])
+ # target['filename'] = space_to_underscore(target['name'])
target['filename'] = ''
if 'description' not in target:
target['description'] = ''
@@ -5271,7 +5043,7 @@ def process_attachment(self, orig_target):
for content_file in target['content file']:
if not isinstance(content_file, str):
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)
+ file_to_read = 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()
@@ -5355,7 +5127,7 @@ def process_attachment(self, orig_target):
if len(template_files) == 1:
the_docx_path = template_files[0]
else:
- the_docx_path = docassemble.base.file_docx.concatenate_files(template_files)
+ the_docx_path = concatenate_files(template_files)
try:
docx_template = DocxTemplate(the_docx_path)
docx_template.render_init()
@@ -5516,6 +5288,10 @@ def process_attachment(self, orig_target):
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 'flattened checkbox label' in target and target['flattened checkbox label'] is not None:
+ options['flattened_checkbox_label'] = TextObject(str(target['flattened checkbox label']), question=self)
+ if 'flattened checkbox unselected label' in target and target['flattened checkbox unselected label'] is not None:
+ options['flattened_checkbox_unselected_label'] = TextObject(str(target['flattened checkbox unselected label']), question=self)
if 'tagged pdf' in target:
if isinstance(target['tagged pdf'], bool):
options['tagged_pdf'] = target['tagged pdf']
@@ -5561,7 +5337,7 @@ def get_question_for_field_with_sub_fields(self, field, user_dict):
if not isinstance(field_list, list):
raise DAError("A code directive that defines items in fields must return a list")
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self.interview, new_interview)
return Question({'question': 'n/a', 'fields': field_list}, new_interview, source=new_interview_source, package=self.package)
@@ -5587,7 +5363,7 @@ def get_fields_and_sub_fields(self, user_dict):
def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, process_list_collect=True, test_for_objects=True):
# logmessage("ask: orig_sought is " + str(orig_sought) + " and q is " + self.name)
- docassemble.base.functions.this_thread.current_question = self
+ this_thread.current_question = self
if the_x != 'None':
exec("x = " + the_x, user_dict)
if len(iterators) > 0:
@@ -5598,18 +5374,18 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
for need_code in self.need:
eval(need_code, user_dict)
for the_field in self.undefine:
- docassemble.base.functions.undefine(the_field)
+ undefine(the_field)
if len(self.reconsider) > 0:
- docassemble.base.functions.reconsider(*[substitute_vars(item, self.is_generic, the_x, iterators) for item in self.reconsider])
+ reconsider(*[substitute_vars(item, self.is_generic, the_x, iterators) for item in self.reconsider])
if self.section:
- docassemble.base.functions.this_thread.current_section = self.section.text(user_dict).strip()
+ this_thread.current_section = self.section.text(user_dict).strip()
question_text = self.content.text(user_dict).rstrip()
if self.breadcrumb is not None:
breadcrumb = self.breadcrumb.text(user_dict).rstrip()
else:
breadcrumb = None
try:
- user_dict['_internal']['event_stack'][docassemble.base.functions.this_thread.current_info['user']['session_uid']][0]['breadcrumb'] = question_text if breadcrumb is None else breadcrumb
+ user_dict['_internal']['event_stack'][this_thread.current_info['user']['session_uid']][0]['breadcrumb'] = question_text if breadcrumb is None else breadcrumb
except:
pass
# logmessage("Asking " + str(question_text))
@@ -5654,7 +5430,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
if forget_prior:
arguments = {'_action': action, '_arguments': arguments}
action = '_da_priority_action'
- action = docassemble.base.functions.url_action(action, **arguments)
+ action = url_action(action, **arguments)
color = item['color'].text(user_dict).strip()
if item['target'] is not None:
target = item['target'].text(user_dict).strip()
@@ -5714,7 +5490,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
if forget_prior:
arguments = {'_action': action, '_arguments': arguments}
action = '_da_priority_action'
- action = docassemble.base.functions.url_action(action, **arguments)
+ action = url_action(action, **arguments)
label = button['label']
extras['action_buttons'].append({'action': action, 'label': label, 'color': color, 'icon': icon, 'placement': placement, 'css_class': css_class, 'target': target})
for item in extras['action_buttons']:
@@ -5797,7 +5573,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
if key in ('pre', 'post', 'footer', 'submit', 'exit link', 'exit label', 'exit url', 'full', 'logo', 'short logo', 'title', 'subtitle', 'tab title', 'short title', 'title url', 'title url opens in other window', 'navigation bar html') and (key + ' text') not in extras:
extras[key + ' text'] = val
if len(self.terms) > 0:
- lang = docassemble.base.functions.get_language()
+ lang = get_language()
extras['terms'] = {}
for termitem, definition in self.terms.items():
if lang in definition['alt_terms']:
@@ -5805,7 +5581,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
else:
extras['terms'][termitem] = {'definition': definition['definition'].text(user_dict)}
if len(self.autoterms) > 0:
- lang = docassemble.base.functions.get_language()
+ lang = get_language()
extras['autoterms'] = {}
for termitem, definition in self.autoterms.items():
if lang in definition['alt_terms']:
@@ -6050,7 +5826,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
else:
extras['allow_emailing'] = eval(self.allow_emailing, user_dict)
if hasattr(self, 'zip_filename'):
- extras['zip_filename'] = docassemble.base.functions.single_paragraph(self.zip_filename.text(user_dict))
+ extras['zip_filename'] = single_paragraph(self.zip_filename.text(user_dict))
if hasattr(self, 'ga_id'):
extras['ga_id'] = self.ga_id.text(user_dict)
if hasattr(self, 'segment') and 'id' in self.segment:
@@ -6076,7 +5852,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
skip_undefined = True
extras['ok'] = {}
for field in self.fields:
- docassemble.base.functions.this_thread.misc['current_field'] = field.number
+ this_thread.misc['current_field'] = field.number
extras['ok'][field.number] = False
if hasattr(field, 'saveas_code'):
failed = False
@@ -6170,8 +5946,8 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
else:
labels[field.number] = field.label.text(user_dict)
extras['ok'][field.number] = True
- if 'current_field' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['current_field']
+ if 'current_field' in this_thread.misc:
+ del this_thread.misc['current_field']
else:
if hasattr(self, 'list_collect') and process_list_collect and eval(self.list_collect, user_dict):
fields_to_scan = self.get_fields_and_sub_fields(user_dict)
@@ -6250,7 +6026,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
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))
+ ask_result = self.ask(user_dict, old_user_dict, the_x, new_iterators, sought, orig_sought, process_list_collect=False, test_for_objects=list_indexno < length_to_use)
if hasattr(self, 'list_collect_label'):
extras['list_message'][list_indexno] = self.list_collect_label.text(user_dict)
else:
@@ -6304,7 +6080,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
for field in self.fields:
if hasattr(field, 'inputtype') and field.inputtype in ('combobox', 'datalist'):
only_empty_fields_exist = False
- docassemble.base.functions.this_thread.misc['current_field'] = field.number
+ this_thread.misc['current_field'] = field.number
if hasattr(field, 'has_code') and field.has_code:
# standalone multiple-choice questions
selectcompute[field.number] = []
@@ -6505,11 +6281,11 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
if complications.search(var) or var not in user_dict:
eval(var, user_dict)
raise CodeExecute(commands_to_run, self)
- if 'current_field' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['current_field']
+ if 'current_field' in this_thread.misc:
+ del this_thread.misc['current_field']
extras['ok'] = {}
for field in self.fields:
- docassemble.base.functions.this_thread.misc['current_field'] = field.number
+ this_thread.misc['current_field'] = field.number
if hasattr(field, 'showif_code'):
result = eval(field.showif_code, user_dict)
if hasattr(field, 'extras') and 'show_if_sign_code' in field.extras and field.extras['show_if_sign_code'] == 0:
@@ -6795,8 +6571,8 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
hints[field.number] = field.hint.text(user_dict)
if hasattr(field, 'helptext'):
helptexts[field.number] = field.helptext.text(user_dict)
- if 'current_field' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['current_field']
+ if 'current_field' in this_thread.misc:
+ del this_thread.misc['current_field']
if len(self.attachments) > 0 or self.compute_attachment is not None:
if hasattr(self, 'email_default'):
the_email_address = self.email_default.text(user_dict).strip()
@@ -6805,7 +6581,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
if hasattr(self, 'email_subject'):
extras['email_subject'] = re.sub(r'[\n\r]+', ' ', self.email_subject.text(user_dict).strip())
if hasattr(self, 'email_body'):
- extras['email_html'] = '' + docassemble.base.filter.markdown_to_html(self.email_body.text(user_dict), status=docassemble.base.functions.this_thread.interview_status, question=self, external=True) + ''
+ extras['email_html'] = '' + markdown_to_html(self.email_body.text(user_dict), status=this_thread.interview_status, question=self, external=True) + ''
extras['email_body'] = BeautifulSoup(extras['email_html'], "html.parser").get_text('\n')
if hasattr(self, 'email_template') and ('email_subject' not in extras or 'email_html' not in extras):
template = eval(self.email_template, user_dict)
@@ -6824,7 +6600,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
for field in self.fields:
if field.number in extras['ok'] and not extras['ok'][field.number]:
continue
- docassemble.base.functions.this_thread.misc['current_field'] = field.number
+ this_thread.misc['current_field'] = field.number
if hasattr(field, 'saveas'):
# m = re.match(r'(.*)\.[^\.]+', from_safeid(field.saveas))
# if m and m.group(1) != 'x':
@@ -6836,8 +6612,8 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
assumed_objects.add(parse_result['objects'][-1])
if len(parse_result['bracket_objects']) > 0:
assumed_objects.add(parse_result['bracket_objects'][-1])
- if 'current_field' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['current_field']
+ if 'current_field' in this_thread.misc:
+ del this_thread.misc['current_field']
for var in assumed_objects:
if complications.search(var) or var not in user_dict:
eval(var, user_dict)
@@ -6861,7 +6637,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p
if self.question_type == 'review' and sought is not None and not hasattr(self, 'review_saveas'):
if 'event_stack' not in user_dict['_internal']:
user_dict['_internal']['event_stack'] = {}
- session_uid = docassemble.base.functions.this_thread.current_info['user']['session_uid']
+ session_uid = this_thread.current_info['user']['session_uid']
if session_uid not in user_dict['_internal']['event_stack']:
user_dict['_internal']['event_stack'][session_uid] = []
already_there = False
@@ -7094,11 +6870,11 @@ def finalize_attachment(self, attachment, result, the_user_dict):
return result
except:
pass
- docassemble.base.functions.this_thread.misc['redact'] = bool(result['redact'])
- docassemble.base.functions.this_thread.misc['attachment_info'] = {k: result[k] for k in ('name', 'filename', 'description', 'update_references', 'convert_to_pdf_a', 'convert_to_tagged_pdf') if k in result}
+ this_thread.misc['redact'] = bool(result['redact'])
+ this_thread.misc['attachment_info'] = {k: result[k] for k in ('name', 'filename', 'description', 'update_references', 'convert_to_pdf_a', 'convert_to_tagged_pdf') if k in result}
if 'language' in attachment['options']:
- old_language = docassemble.base.functions.get_language()
- docassemble.base.functions.set_language(attachment['options']['language'])
+ old_language = get_language()
+ set_language(attachment['options']['language'])
else:
old_language = None
try:
@@ -7107,13 +6883,13 @@ def finalize_attachment(self, attachment, result, the_user_dict):
with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=result['raw'], delete=False) as the_temp:
with open(the_temp.name, 'w', encoding='utf-8') as the_file:
the_file.write(result['markdown'][doc_format].lstrip("\n"))
- result['file'][doc_format], result['extension'][doc_format], result['mimetype'][doc_format] = docassemble.base.functions.server.save_numbered_file(result['filename'] + result['raw'], the_temp.name, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ result['file'][doc_format], result['extension'][doc_format], result['mimetype'][doc_format] = save_numbered_file(result['filename'] + result['raw'], the_temp.name, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
result['content'][doc_format] = result['markdown'][doc_format].lstrip("\n")
elif doc_format == 'md':
with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix='md', delete=False) as the_temp:
with open(the_temp.name, 'w', encoding='utf-8') as the_file:
the_file.write(result['markdown'][doc_format].lstrip("\n"))
- result['file'][doc_format], result['extension'][doc_format], result['mimetype'][doc_format] = docassemble.base.functions.server.save_numbered_file(result['filename'] + '.md', the_temp.name, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ result['file'][doc_format], result['extension'][doc_format], result['mimetype'][doc_format] = save_numbered_file(result['filename'] + '.md', the_temp.name, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
result['content'][doc_format] = result['markdown'][doc_format].lstrip("\n")
elif doc_format in ('pdf', 'rtf', 'rtf to docx', 'tex', 'docx'):
if 'fields' in attachment['options']:
@@ -7122,33 +6898,33 @@ def finalize_attachment(self, attachment, result, the_user_dict):
default_export_value = attachment['options']['checkbox_export_value'].text(the_user_dict).strip()
else:
default_export_value = None
- docassemble.base.functions.set_context('pdf')
+ 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 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.get(doc_format, doc_format), the_pdf_file, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ the_pdf_file = 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'], flattened_checkbox_label=result.get('flattened_checkbox_label', None), flattened_checkbox_unselected_label=result.get('flattened_checkbox_unselected_label', None))
+ result['file'][doc_format], result['extension'][doc_format], result['mimetype'][doc_format] = save_numbered_file(result['filename'] + '.' + extension_of_doc_format.get(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', '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()
+ reset_context()
elif (doc_format == 'docx' or (doc_format == 'pdf' and 'docx' not in result['formats_to_use'])) and 'docx_template_file' in attachment['options']:
# logmessage("field_data is " + repr(result['field_data']))
if result['template'].current_rendering_part is None:
result['template'].current_rendering_part = result['template'].docx._part
- docassemble.base.functions.set_context('docx', template=result['template'])
- docassemble.base.functions.this_thread.misc['docx_subdocs'] = []
- docassemble.base.functions.this_thread.misc['auto jinja filter'] = []
+ set_context('docx', template=result['template'])
+ this_thread.misc['docx_subdocs'] = []
+ this_thread.misc['auto jinja filter'] = []
if 'auto jinja filter' in self.interview.options:
for item in self.interview.options['auto jinja filter']:
- docassemble.base.functions.this_thread.misc['auto jinja filter'].append(eval(item, the_user_dict))
+ this_thread.misc['auto jinja filter'].append(eval(item, the_user_dict))
try:
the_template = result['template']
template_loop_count = 0
while True: # Rerender if there's a subdoc using include_docx_template
- old_count = docassemble.base.functions.this_thread.misc.get('docx_include_count', 0)
+ old_count = this_thread.misc.get('docx_include_count', 0)
the_template.render(result['field_data'], jinja_env=custom_jinja_env(skip_undefined=attachment['options']['skip_undefined']))
- if docassemble.base.functions.this_thread.misc.get('docx_include_count', 0) > old_count and template_loop_count < 10:
+ if this_thread.misc.get('docx_include_count', 0) > old_count and template_loop_count < 10:
# There's another template included
with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".docx", delete=False) as new_template_file:
the_template.save(new_template_file.name) # Save and refresh the template
@@ -7162,12 +6938,12 @@ def finalize_attachment(self, attachment, result, the_user_dict):
the_template.da_hyperlink_style = 'InternetLink'
else:
the_template.da_hyperlink_style = None
- docassemble.base.functions.this_thread.misc['docx_template'] = the_template
+ this_thread.misc['docx_template'] = the_template
template_loop_count += 1
else:
break
# Copy over images, etc from subdoc to master template
- # subdocs = docassemble.base.functions.this_thread.misc.get('docx_subdocs', []) # Get the subdoc file list
+ # subdocs = this_thread.misc.get('docx_subdocs', []) # Get the subdoc file list
# the_template_docx = the_template.docx
except TemplateError as the_error:
@@ -7179,19 +6955,19 @@ def finalize_attachment(self, attachment, result, the_user_dict):
the_error.filename = ', '.join(docx_paths)
# logmessage("TemplateError:\n" + traceback.format_exc())
raise the_error
- docassemble.base.functions.reset_context()
+ reset_context()
with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".docx", delete=False) as docx_file:
the_template.save(docx_file.name)
- docassemble.base.file_docx.fix_docx(docx_file.name)
+ fix_docx(docx_file.name)
if result['update_references']:
- docassemble.base.pandoc.update_references(docx_file.name) # does this update refs twice?
+ update_references(docx_file.name) # does this update refs twice?
if 'pdf' in result['formats_to_use']:
with tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".pdf", delete=False) as pdf_file:
- if not docassemble.base.pandoc.word_to_pdf(docx_file.name, 'docx', pdf_file.name, pdfa=result['convert_to_pdf_a'], password=result['password'], update_refs=result['update_references'], tagged=result['convert_to_tagged_pdf'], filename=result['filename']):
+ if not word_to_pdf(docx_file.name, 'docx', pdf_file.name, pdfa=result['convert_to_pdf_a'], password=result['password'], update_refs=result['update_references'], tagged=result['convert_to_tagged_pdf'], filename=result['filename']):
raise DAException('Failure to convert DOCX to PDF')
- result['file']['pdf'], result['extension']['pdf'], result['mimetype']['pdf'] = docassemble.base.functions.server.save_numbered_file(result['filename'] + '.pdf', pdf_file.name, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ result['file']['pdf'], result['extension']['pdf'], result['mimetype']['pdf'] = save_numbered_file(result['filename'] + '.pdf', pdf_file.name, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
if 'docx' in result['formats_to_use']:
- result['file']['docx'], result['extension']['docx'], result['mimetype']['docx'] = docassemble.base.functions.server.save_numbered_file(result['filename'] + '.docx', docx_file.name, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ result['file']['docx'], result['extension']['docx'], result['mimetype']['docx'] = save_numbered_file(result['filename'] + '.docx', docx_file.name, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
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']:
if key in result:
del result[key]
@@ -7224,10 +7000,10 @@ def finalize_attachment(self, attachment, result, the_user_dict):
converter.template_file = self.interview.attachment_options['template_file'].path(the_user_dict=the_user_dict)
converter.metadata = result['metadata']
converter.convert(self)
- 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.get(doc_format, doc_format), converter.output_filename, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ result['file'][doc_format], result['extension'][doc_format], result['mimetype'][doc_format] = save_numbered_file(result['filename'] + '.' + extension_of_doc_format.get(doc_format, doc_format), converter.output_filename, yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
result['content'][doc_format] = result['markdown'][doc_format]
elif doc_format == 'html':
- result['content'][doc_format] = docassemble.base.filter.markdown_to_html(result['markdown'][doc_format], use_pandoc=True, question=self)
+ result['content'][doc_format] = markdown_to_html(result['markdown'][doc_format], use_pandoc=True, question=self)
elif doc_format == 'md':
result['content'][doc_format] = result['markdown'][doc_format]
if 'manual' in result:
@@ -7258,13 +7034,13 @@ def finalize_attachment(self, attachment, result, the_user_dict):
if attachment['variable_name']:
the_string = "from docassemble.base.util import DAFile, DAFileCollection"
exec(the_string, the_user_dict)
- variable_name = substitute_vars_from_user_dict(docassemble.base.functions.intrinsic_name_of(attachment['variable_name'], the_user_dict=the_user_dict), the_user_dict, is_generic=self.is_generic)
+ variable_name = substitute_vars_from_user_dict(intrinsic_name_of(attachment['variable_name'], the_user_dict=the_user_dict), the_user_dict, is_generic=self.is_generic)
the_string = variable_name + " = DAFileCollection(" + repr(variable_name) + ")"
exec(the_string, the_user_dict)
the_name = attachment['name'].text(the_user_dict).strip()
- the_filename = docassemble.base.functions.secure_filename_unicode_ok(attachment['filename'].text(the_user_dict).strip())
+ the_filename = secure_filename_unicode_ok(attachment['filename'].text(the_user_dict).strip())
if the_filename == '':
- the_filename = docassemble.base.functions.secure_filename_unicode_ok(docassemble.base.functions.space_to_underscore(the_name))
+ the_filename = secure_filename_unicode_ok(space_to_underscore(the_name))
the_user_dict['_attachment_info'] = {'name': the_name, 'filename': the_filename, 'description': attachment['description'].text(the_user_dict), 'valid_formats': result['valid_formats'], 'formats': result['formats_to_use'], 'attachment': {'name': attachment['question_name'], 'number': attachment['indexno']}, 'extension': result.get('extension', {}), 'mimetype': result.get('mimetype', {}), 'content': result.get('content', {}), 'markdown': result.get('markdown', {}), 'metadata': result.get('metadata', {}), 'convert_to_pdf_a': result.get('convert_to_pdf_a', False), 'convert_to_tagged_pdf': result.get('convert_to_tagged_pdf', False), 'orig_variable_name': result.get('orig_variable_name', None), 'raw': result['raw'], 'permissions': result.get('permissions', None)}
if len(manual_files) > 0:
the_user_dict['_attachment_info']['manual_formats'] = result['manual_formats']
@@ -7273,7 +7049,7 @@ def finalize_attachment(self, attachment, result, the_user_dict):
for doc_format in result['file']:
variable_string = variable_name + '.' + extension_of_doc_format.get(doc_format, doc_format)
# filename = result['filename'] + '.' + doc_format
- # file_number, extension, mimetype = docassemble.base.functions.server.save_numbered_file(filename, result['file'][doc_format], yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ # file_number, extension, mimetype = save_numbered_file(filename, result['file'][doc_format], yaml_file_name=self.interview.source.path) # pylint: disable=assignment-from-none,unpacking-non-sequence
if result['file'][doc_format] is None:
raise DAError("Could not save numbered file")
if doc_format in manual_files:
@@ -7325,20 +7101,20 @@ def finalize_attachment(self, attachment, result, the_user_dict):
exec(the_string, the_user_dict)
except:
if old_language is not None:
- docassemble.base.functions.set_language(old_language)
- docassemble.base.functions.this_thread.misc.pop('redact', None)
- docassemble.base.functions.this_thread.misc.pop('attachment_info', None)
+ set_language(old_language)
+ this_thread.misc.pop('redact', None)
+ this_thread.misc.pop('attachment_info', None)
raise
if old_language is not None:
- docassemble.base.functions.set_language(old_language)
- docassemble.base.functions.this_thread.misc.pop('redact', None)
- docassemble.base.functions.this_thread.misc.pop('attachment_info', None)
+ set_language(old_language)
+ this_thread.misc.pop('redact', None)
+ this_thread.misc.pop('attachment_info', None)
return result
def prepare_attachment(self, attachment, the_user_dict):
if 'language' in attachment['options']:
- old_language = docassemble.base.functions.get_language()
- docassemble.base.functions.set_language(attachment['options']['language'])
+ old_language = get_language()
+ set_language(attachment['options']['language'])
else:
old_language = None
if isinstance(attachment['valid_formats'], CodeType):
@@ -7353,9 +7129,9 @@ def prepare_attachment(self, attachment, the_user_dict):
try:
the_name = attachment['name'].text(the_user_dict).strip()
the_filename = attachment['filename'].text(the_user_dict).strip()
- the_filename = docassemble.base.functions.secure_filename_unicode_ok(the_filename)
+ the_filename = secure_filename_unicode_ok(the_filename)
if the_filename == '':
- the_filename = docassemble.base.functions.secure_filename_unicode_ok(docassemble.base.functions.space_to_underscore(the_name))
+ the_filename = secure_filename_unicode_ok(space_to_underscore(the_name))
result = {'name': the_name, 'filename': the_filename, 'description': attachment['description'].text(the_user_dict), 'valid_formats': copy.deepcopy(valid_formats)}
actual_extension = attachment['raw']
if attachment['content'] is None and 'content file code' in attachment['options']:
@@ -7379,7 +7155,7 @@ def prepare_attachment(self, attachment, the_user_dict):
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)
+ the_filename = package_template_filename(the_filename, package=self.package)
else:
the_filename = None
if the_filename is None or not os.path.isfile(the_filename):
@@ -7401,8 +7177,16 @@ def prepare_attachment(self, attachment, the_user_dict):
result['editable'] = eval(attachment['options']['editable'], the_user_dict)
else:
result['editable'] = True
- docassemble.base.functions.this_thread.misc['redact'] = bool(result['redact'])
- docassemble.base.functions.this_thread.misc['attachment_info'] = {k: result[k] for k in ('name', 'filename', 'description', 'update_references', 'convert_to_pdf_a', 'convert_to_tagged_pdf') if k in result}
+ if 'flattened_checkbox_label' in attachment['options']:
+ result['flattened_checkbox_label'] = attachment['options']['flattened_checkbox_label'].text(the_user_dict)
+ else:
+ result['flattened_checkbox_label'] = None
+ if 'flattened_checkbox_unselected_label' in attachment['options']:
+ result['flattened_checkbox_unselected_label'] = attachment['options']['flattened_checkbox_unselected_label'].text(the_user_dict)
+ else:
+ result['flattened_checkbox_unselected_label'] = None
+ this_thread.misc['redact'] = bool(result['redact'])
+ this_thread.misc['attachment_info'] = {k: result[k] for k in ('name', 'filename', 'description', 'update_references', 'convert_to_pdf_a', 'convert_to_tagged_pdf') if k in result}
result['markdown'] = {}
result['content'] = {}
result['extension'] = {}
@@ -7411,7 +7195,7 @@ def prepare_attachment(self, attachment, the_user_dict):
if attachment['raw']:
if '.' in the_filename:
m = re.search(r'(.*)(\..*)', the_filename)
- result['filename'] = docassemble.base.functions.secure_filename_unicode_ok(m.group(1))
+ result['filename'] = secure_filename_unicode_ok(m.group(1))
actual_extension = m.group(2)
result['raw'] = actual_extension
result['formats_to_use'] = ['raw']
@@ -7525,7 +7309,7 @@ def prepare_attachment(self, attachment, the_user_dict):
if len(docx_paths) == 1:
docx_path = docx_paths[0]
else:
- docx_path = docassemble.base.file_docx.concatenate_files(docx_paths)
+ docx_path = concatenate_files(docx_paths)
result['template'] = DocxTemplate(docx_path)
result['template'].render_init()
if result['hyperlink_style'] and result['hyperlink_style'] in result['template'].docx.styles:
@@ -7538,7 +7322,7 @@ def prepare_attachment(self, attachment, the_user_dict):
result['template'].da_hyperlink_style = None
if result['template'].current_rendering_part is None:
result['template'].current_rendering_part = result['template'].docx._part
- docassemble.base.functions.set_context('docx', template=result['template'])
+ set_context('docx', template=result['template'])
if isinstance(attachment['options']['fields'], str):
result['field_data'] = the_user_dict
else:
@@ -7574,7 +7358,7 @@ def prepare_attachment(self, attachment, the_user_dict):
elif isinstance(val, RawValue):
result['field_data'][key] = val.value
else:
- result['field_data'][key] = docassemble.base.file_docx.transform_for_docx(val)
+ result['field_data'][key] = transform_for_docx(val)
else:
raise DAError("code in an attachment returned something other than a dictionary")
if 'raw code dict' in attachment['options']:
@@ -7604,10 +7388,10 @@ def prepare_attachment(self, attachment, the_user_dict):
elif isinstance(val, RawValue):
result['field_data'][varname] = val.value
else:
- result['field_data'][varname] = docassemble.base.file_docx.transform_for_docx(val)
- docassemble.base.functions.reset_context()
+ result['field_data'][varname] = transform_for_docx(val)
+ reset_context()
elif doc_format == 'pdf' and 'fields' in attachment['options'] and 'pdf_template_file' in attachment['options']:
- docassemble.base.functions.set_context('pdf')
+ set_context('pdf')
result['data_strings'] = []
result['images'] = []
if isinstance(attachment['options']['fields'], dict):
@@ -7635,9 +7419,9 @@ def prepare_attachment(self, attachment, the_user_dict):
m = re.search(r'\[FILE ([^\]]+)\]', answer)
if m:
file_reference = re.sub(r'[ ,].*', '', m.group(1))
- file_info = docassemble.base.functions.server.file_finder(file_reference, question=self)
+ file_info = file_finder(file_reference, question=self)
if 'path' in file_info and 'extension' in file_info:
- docassemble.base.filter.convert_svg_to_png(file_info)
+ convert_svg_to_png(file_info)
result['images'].append((key, file_info))
else:
m = re.search(r'\[QR ([^\]]+)\]', answer)
@@ -7677,9 +7461,9 @@ def prepare_attachment(self, attachment, the_user_dict):
m = re.search(r'\[FILE ([^\]]+)\]', val)
if m:
file_reference = re.sub(r'[ ,].*', '', m.group(1))
- file_info = docassemble.base.functions.server.file_finder(file_reference, question=self)
+ file_info = file_finder(file_reference, question=self)
if 'path' in file_info and 'extension' in file_info:
- docassemble.base.filter.convert_svg_to_png(file_info)
+ convert_svg_to_png(file_info)
result['images'].append((key, file_info))
else:
m = re.search(r'\[QR ([^\]]+)\]', val)
@@ -7720,9 +7504,9 @@ def prepare_attachment(self, attachment, the_user_dict):
m = re.search(r'\[FILE ([^\]]+)\]', val)
if m:
file_reference = re.sub(r'[ ,].*', '', m.group(1))
- file_info = docassemble.base.functions.server.file_finder(file_reference, question=self)
+ file_info = file_finder(file_reference, question=self)
if 'path' in file_info and 'extension' in file_info:
- docassemble.base.filter.convert_svg_to_png(file_info)
+ convert_svg_to_png(file_info)
result['images'].append((key, file_info))
else:
m = re.search(r'\[QR ([^\]]+)\]', val)
@@ -7761,9 +7545,9 @@ def prepare_attachment(self, attachment, the_user_dict):
m = re.search(r'\[FILE ([^\]]+)\]', val)
if m:
file_reference = re.sub(r'[ ,].*', '', m.group(1))
- file_info = docassemble.base.functions.server.file_finder(file_reference, question=self)
+ file_info = file_finder(file_reference, question=self)
if 'path' in file_info and 'extension' in file_info:
- docassemble.base.filter.convert_svg_to_png(file_info)
+ convert_svg_to_png(file_info)
result['images'].append((key, file_info))
else:
m = re.search(r'\[QR ([^\]]+)\]', val)
@@ -7774,24 +7558,24 @@ def prepare_attachment(self, attachment, the_user_dict):
result['images'].append((key, {'fullpath': the_image.name}))
else:
result['data_strings'].append((key, val))
- docassemble.base.functions.reset_context()
+ reset_context()
elif doc_format in ('raw', 'md'):
- docassemble.base.functions.set_context(doc_format)
+ set_context(doc_format)
the_markdown = the_content.text(the_user_dict)
result['markdown'][doc_format] = the_markdown
- docassemble.base.functions.reset_context()
+ reset_context()
else:
modified_metadata = {"syslang": get_language()}
if result['convert_to_tagged_pdf']:
modified_metadata['taggedpdf'] = "true"
for key, data in result['metadata'].items():
if re.search(r'Footer|Header', key) and 'Lines' not in key:
- # modified_metadata[key] = docassemble.base.filter.metadata_filter(data, doc_format) + str('[END]')
+ # modified_metadata[key] = metadata_filter(data, doc_format) + str('[END]')
modified_metadata[key] = data + str('[END]')
else:
modified_metadata[key] = data
the_markdown = '---\n' + altyaml.dump_to_string(modified_metadata) + "\n...\n"
- docassemble.base.functions.set_context('pandoc ' + doc_format)
+ set_context('pandoc ' + doc_format)
the_markdown += the_content.text(the_user_dict)
# logmessage("Markdown is:\n" + repr(the_markdown) + "END")
do_not_scan_for_emojis = bool(re.search(r'\[NO_EMOJIS\]', the_markdown))
@@ -7800,27 +7584,27 @@ def prepare_attachment(self, attachment, the_user_dict):
elif emoji_match.search(the_markdown) and len(self.interview.images) > 0:
the_markdown = emoji_match.sub(emoji_matcher_insert(self), the_markdown)
result['markdown'][doc_format] = the_markdown
- docassemble.base.functions.reset_context()
+ reset_context()
elif doc_format in ['html']:
- docassemble.base.functions.set_context('html')
+ set_context('html')
result['markdown'][doc_format] = the_content.text(the_user_dict)
do_not_scan_for_emojis = bool(re.search(r'\[NO_EMOJIS\]', result['markdown'][doc_format]))
if do_not_scan_for_emojis:
result['markdown'][doc_format] = re.sub(r'\[NO_EMOJIS\]\s*', r'', result['markdown'][doc_format])
elif emoji_match.search(result['markdown'][doc_format]) and len(self.interview.images) > 0:
result['markdown'][doc_format] = emoji_match.sub(emoji_matcher_html(self), result['markdown'][doc_format])
- docassemble.base.functions.reset_context()
+ reset_context()
# logmessage("output was:\n" + repr(result['content'][doc_format]))
except:
if old_language is not None:
- docassemble.base.functions.set_language(old_language)
- docassemble.base.functions.this_thread.misc.pop('redact', None)
- docassemble.base.functions.this_thread.misc.pop('attachment_info', None)
+ set_language(old_language)
+ this_thread.misc.pop('redact', None)
+ this_thread.misc.pop('attachment_info', None)
raise
if old_language is not None:
- docassemble.base.functions.set_language(old_language)
- docassemble.base.functions.this_thread.misc.pop('redact', None)
- docassemble.base.functions.this_thread.misc.pop('attachment_info', None)
+ set_language(old_language)
+ this_thread.misc.pop('redact', None)
+ this_thread.misc.pop('attachment_info', None)
if 'manual' in attachment['options']:
result['manual'] = {extension: eval(expression, the_user_dict) for extension, expression in attachment['options']['manual'].items()}
if 'manual code' in attachment['options']:
@@ -7904,36 +7688,11 @@ def process_selections_manual(self, data):
def emoji_matcher_insert(obj):
- return (lambda x: docassemble.base.filter.emoji_insert(x.group(1), images=obj.interview.images))
+ return (lambda x: emoji_insert(x.group(1), images=obj.interview.images))
def emoji_matcher_html(obj):
- return (lambda x: docassemble.base.filter.emoji_html(x.group(1), images=obj.interview.images))
-
-
-def question_path_options(path):
- n = 0
- while n < 3:
- if n == 0:
- yield docassemble.base.functions.package_question_filename(path)
- elif n == 1:
- yield docassemble.base.functions.standard_question_filename(path)
- elif n == 2:
- yield docassemble.base.functions.server.absolute_filename(path)
- n += 1
-
-
-def interview_source_from_string(path, **kwargs):
- if path is None:
- raise DAError("Passed None to interview_source_from_string")
- # logmessage("Trying to find " + path)
- path = re.sub(r'(docassemble.playground[0-9]+[^:]*:)data/questions/(.*)', r'\1\2', path)
- for the_filename in question_path_options(path):
- if the_filename is not None:
- new_source = InterviewSourceFile(filepath=the_filename, path=path)
- if new_source.update(**kwargs):
- return new_source
- raise DANotFoundError("Interview " + str(path) + " not found")
+ return (lambda x: emoji_html(x.group(1), images=obj.interview.images))
def is_boolean(field_data):
@@ -8016,7 +7775,7 @@ def illegal_variable_name(var):
t = ast.parse(var)
except:
return True
- detector = docassemble.base.astparser.detectIllegal()
+ detector = DetectIllegal()
detector.visit(t)
return detector.illegal
@@ -8086,28 +7845,28 @@ def format_yaml_mark(mark, filename, line_number):
return str(mark)
-def format_yaml_errmess(errMess, filename, line_number):
- if isinstance(errMess, ruamel.yaml.error.MarkedYAMLError):
+def format_yaml_errmess(error_message, filename, line_number):
+ if isinstance(error_message, ruamel.yaml.error.MarkedYAMLError):
lines = []
- if errMess.context is not None:
- lines.append(errMess.context)
- if errMess.context_mark is not None and (
- errMess.problem is None
- or errMess.problem_mark is None
- or errMess.context_mark.name != errMess.problem_mark.name
- or errMess.context_mark.line != errMess.problem_mark.line
- or errMess.context_mark.column != errMess.problem_mark.column
+ if error_message.context is not None:
+ lines.append(error_message.context)
+ if error_message.context_mark is not None and (
+ error_message.problem is None
+ or error_message.problem_mark is None
+ or error_message.context_mark.name != error_message.problem_mark.name
+ or error_message.context_mark.line != error_message.problem_mark.line
+ or error_message.context_mark.column != error_message.problem_mark.column
):
- lines.append(format_yaml_mark(errMess.context_mark, filename, line_number))
- if errMess.problem is not None:
- lines.append(errMess.problem)
- if errMess.problem_mark is not None:
- lines.append(format_yaml_mark(errMess.problem_mark, filename, line_number))
- if errMess.note is not None and errMess.note:
- note = textwrap.dedent(errMess.note)
+ lines.append(format_yaml_mark(error_message.context_mark, filename, line_number))
+ if error_message.problem is not None:
+ lines.append(error_message.problem)
+ if error_message.problem_mark is not None:
+ lines.append(format_yaml_mark(error_message.problem_mark, filename, line_number))
+ if error_message.note is not None and error_message.note:
+ note = textwrap.dedent(error_message.note)
lines.append(note)
return '\n'.join(lines)
- return str(errMess)
+ return str(error_message)
class Interview:
@@ -8181,7 +7940,7 @@ def __init__(self, **kwargs):
self.consolidated_metadata = {}
self.issue = {}
self.custom_data_types = set()
- self.default_language = docassemble.base.functions.server.default_language
+ self.default_language = get_default_language()
if 'source' in kwargs:
self.read_from(kwargs['source'])
self.cross_reference_dependencies()
@@ -8335,9 +8094,9 @@ def get_bootstrap_theme(self):
if self.bootstrap_theme is None:
return None
if not hasattr(self, 'bootstrap_theme_package'):
- result = docassemble.base.functions.server.url_finder(self.bootstrap_theme, _package=self.source.package)
+ result = url_finder(self.bootstrap_theme, _package=self.source.package)
else:
- result = docassemble.base.functions.server.url_finder(self.bootstrap_theme, _package=self.bootstrap_theme_package)
+ result = url_finder(self.bootstrap_theme, _package=self.bootstrap_theme_package)
return result
def get_tags(self, the_user_dict):
@@ -8552,39 +8311,39 @@ def read_from(self, source):
if document is not None:
question = Question(document, self, source=source, package=source_package, source_code=source_code, line_number=line_number)
self.names_used.update(question.fields_used)
- except BaseException as errMess:
+ except BaseException as err_mess:
# logmessage(str(source_code))
try:
- logmessage(f'Interview: error reading YAML file {source.path} in the block on line {line_number}\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n{format_yaml_errmess(errMess, source.path, line_number)}')
+ logmessage(f'Interview: error reading YAML file {source.path} in the block on line {line_number}\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n{format_yaml_errmess(err_mess, source.path, line_number)}')
except:
try:
- logmessage(f'Interview: error reading YAML file {source.path} in the block on line {line_number}. Error was:\n\n{errMess}')
+ logmessage(f'Interview: error reading YAML file {source.path} in the block on line {line_number}. Error was:\n\n{err_mess}')
except:
- logmessage(f'Interview: error reading YAML file {source.path} in the block on line {line_number}. Error type was:\n\n' + errMess.__class__.__name__)
+ logmessage(f'Interview: error reading YAML file {source.path} in the block on line {line_number}. Error type was:\n\n' + err_mess.__class__.__name__)
self.success = False
else:
try:
document = safeyaml.load(source_code)
- except BaseException as errMess:
+ except BaseException as err_mess:
self.success = False
try:
- error_to_raise = DASourceError(f'Error reading YAML file {source.path} in the block on line {line_number}\n\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n{format_yaml_errmess(errMess, source.path, line_number)}')
+ error_to_raise = DASourceError(f'Error reading YAML file {source.path} in the block on line {line_number}\n\nDocument source code was:\n\n---\n{source_code.strip()}\n---\n\nError was:\n\n{format_yaml_errmess(err_mess, source.path, line_number)}')
except:
- 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__))
+ 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(err_mess.__class__.__name__))
raise error_to_raise
if document is not None:
try:
question = Question(document, self, source=source, package=source_package, source_code=source_code, line_number=line_number)
self.names_used.update(question.fields_used)
- except SyntaxException as qError:
+ except SyntaxException as question_error:
self.success = False
- 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:
+ raise DASourceError(f"Syntax Exception: {question_error}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}")
+ except CompileException as question_error:
self.success = False
- 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:
+ raise DASourceError(f"Compile Exception: {question_error}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}")
+ except SyntaxError as question_error:
self.success = False
- raise DASourceError(f"Syntax Error: {qError}\n\nIn file {source.path} in the block on line {line_number} from package {source_package}:\n{source_code}")
+ raise DASourceError(f"Syntax Error: {question_error}\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'):
@@ -8621,13 +8380,13 @@ def read_from(self, source):
for metadata in self.metadata:
if 'social' in metadata and isinstance(metadata['social'], dict):
if 'image' in metadata['social'] and isinstance(metadata['social']['image'], str):
- metadata['social']['image'] = docassemble.base.functions.server.url_finder(metadata['social']['image'], _package=metadata['_origin_package'], _external=True)
+ metadata['social']['image'] = url_finder(metadata['social']['image'], _package=metadata['_origin_package'], _external=True)
if metadata['social']['image'] is None:
logmessage("Invalid image reference in social meta tags")
del metadata['social']['image']
for key, subkey in (('og', 'image'), ('twitter', 'image')):
if key in metadata['social'] and isinstance(metadata['social'][key], dict) and subkey in metadata['social'][key] and isinstance(metadata['social'][key][subkey], str):
- metadata['social'][key][subkey] = docassemble.base.functions.server.url_finder(metadata['social'][key][subkey], _package=metadata['_origin_package'], _external=True)
+ metadata['social'][key][subkey] = url_finder(metadata['social'][key][subkey], _package=metadata['_origin_package'], _external=True)
if metadata['social'][key][subkey] is None:
logmessage("Invalid image reference in social meta tags")
del metadata['social'][key][subkey]
@@ -8655,7 +8414,7 @@ def read_from(self, source):
self.default_title[lang][title_abb] = str(val).strip()
else:
self.default_title['*'][title_abb] = str(metadata[title_name]).strip()
- for lang, parts in docassemble.base.functions.server.main_page_parts.items():
+ for lang, parts in get_main_page_parts().items():
if lang not in self.default_title:
self.default_title[lang] = {}
for title_name, title_abb in mapping:
@@ -8758,14 +8517,14 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
# if interview_status.current_info['url'] is not None:
# user_dict['_internal']['url'] = interview_status.current_info['url']
interview_status.set_tracker(user_dict['_internal']['tracker'])
- # docassemble.base.functions.reset_local_variables()
+ # reset_local_variables()
interview_status.current_info.update({'default_role': self.default_role})
- docassemble.base.functions.this_thread.misc['reconsidered'] = set()
- docassemble.base.functions.this_thread.current_package = self.source.package
- docassemble.base.functions.this_thread.current_info = interview_status.current_info
- docassemble.base.functions.this_thread.interview = self
- docassemble.base.functions.this_thread.interview_status = interview_status
- docassemble.base.functions.this_thread.internal = user_dict['_internal']
+ this_thread.misc['reconsidered'] = set()
+ this_thread.current_package = self.source.package
+ this_thread.current_info = interview_status.current_info
+ this_thread.interview = self
+ this_thread.interview_status = interview_status
+ this_thread.internal = user_dict['_internal']
if user_dict['nav'].sections is None:
user_dict['nav'].sections = self.sections
if hasattr(self, 'sections_progressive'):
@@ -8834,9 +8593,9 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
while True:
number_loops += 1
if number_loops > self.loop_limit:
- docassemble.base.functions.wrap_up()
+ wrap_up()
raise DASourceError("There appears to be a circularity. Variables involved: " + ", ".join(variables_sought) + ".")
- docassemble.base.functions.reset_gathering_mode()
+ reset_gathering_mode()
if 'action' in interview_status.current_info:
# logmessage("assemble: there is an action in the current_info: " + repr(interview_status.current_info['action']))
if interview_status.current_info['action'] in ('_da_list_remove', '_da_list_add', '_da_list_complete'):
@@ -8862,7 +8621,7 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
if force_question is not None:
if self.debug and question is not None:
interview_status.seeking.append({'question': question, 'reason': 'multiple choice question', 'time': time.time()})
- docassemble.base.functions.this_thread.current_question = force_question
+ this_thread.current_question = force_question
interview_status.populate(force_question.ask(user_dict, old_user_dict, 'None', [], None, None))
raise MandatoryQuestion()
if not self.calls_process_action:
@@ -8873,7 +8632,7 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
# logmessage("Running some initial code:\n\n" + question.sourcecode)
if self.debug:
interview_status.seeking.append({'question': question, 'reason': 'initial', 'time': time.time()})
- docassemble.base.functions.this_thread.current_question = question
+ this_thread.current_question = question
question.exec_setup_mandatory(user_dict)
exec_with_trap(question, user_dict)
continue
@@ -8891,9 +8650,9 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
gathered = eval(question.gathered, user_dict)
thename = from_safeid(question.fields[0].saveas)
if question.use_objects == 'objects':
- user_dict['_DADATA'] = docassemble.base.util.objects_from_data(recursive_eval_dataobject(question.fields[0].data, user_dict), recursive=True, gathered=gathered, name=thename, package=question.package)
+ user_dict['_DADATA'] = objects_from_data(recursive_eval_dataobject(question.fields[0].data, user_dict), recursive=True, gathered=gathered, name=thename, package=question.package)
elif question.use_objects:
- user_dict['_DADATA'] = docassemble.base.util.objects_from_structure(recursive_eval_dataobject(question.fields[0].data, user_dict), root=thename, gathered=gathered)
+ user_dict['_DADATA'] = objects_from_structure(recursive_eval_dataobject(question.fields[0].data, user_dict), root=thename, gathered=gathered)
else:
user_dict['_DADATA'] = recursive_eval_dataobject(question.fields[0].data, user_dict)
the_string = thename + ' = _DADATA'
@@ -8910,9 +8669,9 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
gathered = eval(question.gathered, user_dict)
thename = from_safeid(question.fields[0].saveas)
if question.use_objects == 'objects':
- user_dict['_DADATAFROMCODE'] = docassemble.base.util.objects_from_data(recursive_eval_data_from_code(question.fields[0].data, user_dict), recursive=True, gathered=gathered, name=thename, package=question.package)
+ user_dict['_DADATAFROMCODE'] = objects_from_data(recursive_eval_data_from_code(question.fields[0].data, user_dict), recursive=True, gathered=gathered, name=thename, package=question.package)
elif question.use_objects:
- user_dict['_DADATAFROMCODE'] = docassemble.base.util.objects_from_structure(recursive_eval_data_from_code(question.fields[0].data, user_dict), root=thename, gathered=gathered)
+ user_dict['_DADATAFROMCODE'] = objects_from_structure(recursive_eval_data_from_code(question.fields[0].data, user_dict), root=thename, gathered=gathered)
else:
user_dict['_DADATAFROMCODE'] = recursive_eval_data_from_code(question.fields[0].data, user_dict)
the_string = thename + ' = _DADATAFROMCODE'
@@ -8938,7 +8697,7 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
if self.debug:
interview_status.seeking.append({'question': question, 'reason': 'objects', 'time': time.time()})
# logmessage("Going into objects")
- docassemble.base.functions.this_thread.current_question = question
+ this_thread.current_question = question
question.exec_setup_mandatory(user_dict)
for keyvalue in question.objects:
for variable in keyvalue:
@@ -8959,7 +8718,7 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
interview_status.seeking.append({'question': question, 'reason': 'mandatory code', 'time': time.time()})
# logmessage("Running some code:\n\n" + question.sourcecode)
# logmessage("Question name is " + question.name)
- docassemble.base.functions.this_thread.current_question = question
+ this_thread.current_question = question
question.exec_setup_mandatory(user_dict)
exec_with_trap(question, user_dict)
# logmessage("Code completed")
@@ -8974,7 +8733,7 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
if self.debug and the_question is not question:
interview_status.seeking.append({'question': the_question, 'reason': 'result of multiple choice', 'time': time.time()})
if the_question.question_type in ["code", "event_code"]:
- docassemble.base.functions.this_thread.current_question = the_question
+ this_thread.current_question = the_question
question.exec_setup_mandatory(user_dict)
exec_with_trap(the_question, user_dict)
interview_status.mark_tentative_as_answered(user_dict)
@@ -8993,12 +8752,12 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
except ForcedReRun:
continue
except (NameError, UnboundLocalError, DAAttributeError, DAIndexError) as the_exception:
- if 'pending_error' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['pending_error']
+ if 'pending_error' in this_thread.misc:
+ del this_thread.misc['pending_error']
# logmessage("Error in " + the_exception.__class__.__name__ + " is " + str(the_exception))
- if self.debug and docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if self.debug and this_thread.evaluation_context == 'docx':
logmessage("NameError exception during document assembly: " + str(the_exception))
- docassemble.base.functions.reset_context()
+ reset_context()
seeking_question = False
if isinstance(the_exception, ForcedNameError):
# logmessage("assemble: got a ForcedNameError for " + str(the_exception.name))
@@ -9023,10 +8782,10 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
user_dict['_internal']['event_stack'][session_uid] = new_items + user_dict['_internal']['event_stack'][session_uid]
if exception_name.startswith('_da_'):
continue
- docassemble.base.functions.this_thread.misc['forgive_missing_question'] = [exception_name]
+ this_thread.misc['forgive_missing_question'] = [exception_name]
if the_exception.arguments is not None:
- docassemble.base.functions.this_thread.current_info.update({'action': exception_name, 'arguments': the_exception.arguments})
- missingVariable = exception_name
+ this_thread.current_info.update({'action': exception_name, 'arguments': the_exception.arguments})
+ missing_variable = exception_name
else:
if type(the_exception) is NameError: # pylint: disable=unidiomatic-typecheck
cl, exc, tb = sys.exc_info()
@@ -9043,9 +8802,9 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
del exc
del tb
follow_mc = True
- missingVariable = extract_missing_name(the_exception)
- variables_sought.add(missingVariable)
- question_result = self.askfor(missingVariable, user_dict, old_user_dict, interview_status, seeking=interview_status.seeking, follow_mc=follow_mc, seeking_question=seeking_question)
+ missing_variable = extract_missing_name(the_exception)
+ variables_sought.add(missing_variable)
+ question_result = self.askfor(missing_variable, user_dict, old_user_dict, interview_status, seeking=interview_status.seeking, follow_mc=follow_mc, seeking_question=seeking_question)
if question_result['type'] in ('continue', 're_run'):
continue
if question_result['type'] == 'refresh':
@@ -9055,14 +8814,14 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
break
except UndefinedError as the_exception:
# logmessage("UndefinedError")
- if self.debug and docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if self.debug and this_thread.evaluation_context == 'docx':
# logmessage(the_exception.__class__.__name__ + " exception during document assembly: " + str(the_exception) + "\n" + traceback.format_exc())
logmessage(the_exception.__class__.__name__ + " exception during document assembly: " + str(the_exception))
- docassemble.base.functions.reset_context()
- missingVariable = extract_missing_name(the_exception)
- # logmessage("extracted " + missingVariable)
- variables_sought.add(missingVariable)
- question_result = self.askfor(missingVariable, user_dict, old_user_dict, interview_status, seeking=interview_status.seeking, follow_mc=True)
+ reset_context()
+ missing_variable = extract_missing_name(the_exception)
+ # logmessage("extracted " + missing_variable)
+ variables_sought.add(missing_variable)
+ question_result = self.askfor(missing_variable, user_dict, old_user_dict, interview_status, seeking=interview_status.seeking, follow_mc=True)
if question_result['type'] in ('continue', 're_run'):
continue
if question_result['type'] == 'refresh':
@@ -9070,128 +8829,128 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
else:
interview_status.populate(question_result)
break
- except CommandError as qError:
+ except CommandError as question_error:
# logmessage("CommandError")
- docassemble.base.functions.reset_context()
- question_data = {'command': qError.return_type, 'sleep': qError.sleep, 'question': qError.question_text, 'subquestion': qError.subquestion_text}
+ reset_context()
+ question_data = {'command': question_error.return_type, 'sleep': question_error.sleep, 'question': question_error.question_text, 'subquestion': question_error.subquestion_text}
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self, new_interview)
new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
new_question.name = "Question_Temp"
interview_status.populate(new_question.ask(user_dict, old_user_dict, 'None', [], None, None))
break
- except ResponseError as qError:
- docassemble.base.functions.reset_context()
+ except ResponseError as question_error:
+ reset_context()
# logmessage("Trapped ResponseError")
question_data = {'extras': {}}
- if hasattr(qError, 'response') and qError.response is not None:
- question_data['response'] = qError.response
- elif hasattr(qError, 'binaryresponse') and qError.binaryresponse is not None:
- question_data['binaryresponse'] = qError.binaryresponse
- elif hasattr(qError, 'filename') and qError.filename is not None:
- question_data['response filename'] = qError.filename
- elif hasattr(qError, 'url') and qError.url is not None:
- question_data['redirect url'] = qError.url
- elif hasattr(qError, 'all_variables') and qError.all_variables:
- if hasattr(qError, 'include_internal'):
- question_data['include_internal'] = qError.include_internal
+ if hasattr(question_error, 'response') and question_error.response is not None:
+ question_data['response'] = question_error.response
+ elif hasattr(question_error, 'binaryresponse') and question_error.binaryresponse is not None:
+ question_data['binaryresponse'] = question_error.binaryresponse
+ elif hasattr(question_error, 'filename') and question_error.filename is not None:
+ question_data['response filename'] = question_error.filename
+ elif hasattr(question_error, 'url') and question_error.url is not None:
+ question_data['redirect url'] = question_error.url
+ elif hasattr(question_error, 'all_variables') and question_error.all_variables:
+ if hasattr(question_error, 'include_internal'):
+ question_data['include_internal'] = question_error.include_internal
question_data['content type'] = 'application/json'
question_data['all_variables'] = True
- elif hasattr(qError, 'nullresponse') and qError.nullresponse:
- question_data['null response'] = qError.nullresponse
- elif hasattr(qError, 'sleep') and qError.sleep:
- question_data['sleep'] = qError.sleep
- if hasattr(qError, 'content_type') and qError.content_type:
- question_data['content type'] = qError.content_type
- if hasattr(qError, 'response_code') and qError.response_code:
- question_data['response code'] = qError.response_code
+ elif hasattr(question_error, 'nullresponse') and question_error.nullresponse:
+ question_data['null response'] = question_error.nullresponse
+ elif hasattr(question_error, 'sleep') and question_error.sleep:
+ question_data['sleep'] = question_error.sleep
+ if hasattr(question_error, 'content_type') and question_error.content_type:
+ question_data['content type'] = question_error.content_type
+ if hasattr(question_error, 'response_code') and question_error.response_code:
+ question_data['response code'] = question_error.response_code
# new_interview = copy.deepcopy(self)
# if self.source is None:
# new_interview_source = InterviewSourceString(content='')
# else:
# new_interview_source = self.source
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self, new_interview)
new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
new_question.name = "Question_Temp"
# the_question = new_question.follow_multiple_choice(user_dict)
interview_status.populate(new_question.ask(user_dict, old_user_dict, 'None', [], None, None))
break
- except BackgroundResponseError as qError:
- docassemble.base.functions.reset_context()
+ except BackgroundResponseError as question_error:
+ reset_context()
# logmessage("Trapped BackgroundResponseError")
question_data = {'extras': {}}
- if hasattr(qError, 'backgroundresponse'):
- question_data['backgroundresponse'] = normalize_background_response(qError.backgroundresponse)
- if hasattr(qError, 'sleep'):
- question_data['sleep'] = qError.sleep
+ if hasattr(question_error, 'backgroundresponse'):
+ question_data['backgroundresponse'] = normalize_background_response(question_error.backgroundresponse)
+ if hasattr(question_error, 'sleep'):
+ question_data['sleep'] = question_error.sleep
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self, new_interview)
new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
new_question.name = "Question_Temp"
interview_status.populate(new_question.ask(user_dict, old_user_dict, 'None', [], None, None))
break
- except BackgroundResponseActionError as qError:
- docassemble.base.functions.reset_context()
+ except BackgroundResponseActionError as question_error:
+ reset_context()
# logmessage("Trapped BackgroundResponseActionError")
question_data = {'extras': {}}
- if hasattr(qError, 'action'):
- question_data['action'] = qError.action
+ if hasattr(question_error, 'action'):
+ question_data['action'] = question_error.action
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self, new_interview)
new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
new_question.name = "Question_Temp"
interview_status.populate(new_question.ask(user_dict, old_user_dict, 'None', [], None, None))
break
- # except SendFileError as qError:
+ # except SendFileError as question_error:
# # logmessage("Trapped SendFileError")
# question_data = {'extras': {}}
- # if hasattr(qError, 'filename') and qError.filename is not None:
- # question_data['response filename'] = qError.filename
- # if hasattr(qError, 'content_type') and qError.content_type:
- # question_data['content type'] = qError.content_type
+ # if hasattr(question_error, 'filename') and question_error.filename is not None:
+ # question_data['response filename'] = question_error.filename
+ # if hasattr(question_error, 'content_type') and question_error.content_type:
+ # question_data['content type'] = question_error.content_type
# new_interview_source = InterviewSourceString(content='')
- # new_interview = new_interview_source.get_interview()
+ # new_interview = Interview(source=new_interview_source)
# new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
# new_question.name = "Question_Temp"
# interview_status.populate(new_question.ask(user_dict, old_user_dict, 'None', [], None))
# break
- except QuestionError as qError:
+ except QuestionError as question_error:
# logmessage("QuestionError")
- docassemble.base.functions.reset_context()
+ reset_context()
question_data = {}
- if qError.question:
- question_data['question'] = qError.question
- if qError.subquestion:
- question_data['subquestion'] = qError.subquestion
- if qError.reload:
- question_data['reload'] = qError.reload
- if qError.dead_end:
+ if question_error.question:
+ question_data['question'] = question_error.question
+ if question_error.subquestion:
+ question_data['subquestion'] = question_error.subquestion
+ if question_error.reload:
+ question_data['reload'] = question_error.reload
+ if question_error.dead_end:
pass
- elif qError.buttons:
- question_data['buttons'] = qError.buttons
+ elif question_error.buttons:
+ question_data['buttons'] = question_error.buttons
else:
buttons = []
- if qError.show_exit is not False and not (qError.show_leave is True and qError.show_exit is None):
+ if question_error.show_exit is not False and not (question_error.show_leave is True and question_error.show_exit is None):
exit_button = {word('Exit'): 'exit'}
- if qError.url:
- exit_button.update({'url': qError.url})
+ if question_error.url:
+ exit_button.update({'url': question_error.url})
buttons.append(exit_button)
- if qError.show_leave:
+ if question_error.show_leave:
leave_button = {word('Leave'): 'leave'}
- if qError.url:
- leave_button.update({'url': qError.url})
+ if question_error.url:
+ leave_button.update({'url': question_error.url})
buttons.append(leave_button)
- if qError.show_restart is not False:
+ if question_error.show_restart is not False:
buttons.append({word('Restart'): 'restart'})
if len(buttons) > 0:
question_data['buttons'] = buttons
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self, new_interview)
new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
new_question.name = "Question_Temp"
@@ -9201,57 +8960,57 @@ 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))
break
except AttributeError as the_error:
- if 'pending_error' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['pending_error']
+ if 'pending_error' in this_thread.misc:
+ del this_thread.misc['pending_error']
# logmessage("Regular attributeerror")
- docassemble.base.functions.reset_context()
+ reset_context()
# logmessage(str(the_error.args))
- docassemble.base.functions.wrap_up()
+ wrap_up()
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()
+ reset_context()
break
except CodeExecute as code_error:
# logmessage("CodeExecute")
- docassemble.base.functions.reset_context()
+ reset_context()
# if self.debug:
# interview_status.seeking.append({'question': question, 'reason': 'mandatory code'})
exec(code_error.compute, user_dict)
code_error.question.mark_as_answered(user_dict)
- except SyntaxException as qError:
+ except SyntaxException as question_error:
# logmessage("SyntaxException")
- docassemble.base.functions.reset_context()
+ reset_context()
the_question = None
try:
the_question = question
except:
pass
- docassemble.base.functions.wrap_up()
+ wrap_up()
if the_question is not None:
- raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
- raise DASourceError("no question available: " + str(qError))
- except CompileException as qError:
+ raise DASourceError(str(question_error) + "\n\n" + str(self.idebug(self.data_for_debug)))
+ raise DASourceError("no question available: " + str(question_error))
+ except CompileException as question_error:
# logmessage("CompileException")
- docassemble.base.functions.reset_context()
+ reset_context()
the_question = None
try:
the_question = question
except:
pass
- docassemble.base.functions.wrap_up()
+ wrap_up()
if the_question is not None:
- raise DASourceError(str(qError) + "\n\n" + str(self.idebug(self.data_for_debug)))
- raise DASourceError("no question available: " + str(qError))
+ raise DASourceError(str(question_error) + "\n\n" + str(self.idebug(self.data_for_debug)))
+ raise DASourceError("no question available: " + str(question_error))
else:
- docassemble.base.functions.wrap_up()
+ 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.')
except BaseException as the_error:
# logmessage("Untrapped exception")
if self.debug:
the_error.interview = self
the_error.interview_status = interview_status
- the_error.user_dict = docassemble.base.functions.serializable_dict(user_dict)
+ the_error.user_dict = serializable_dict(user_dict)
if not hasattr(the_error, '__traceback__'):
cl, exc, tb = sys.exc_info()
the_error.__traceback__ = tb
@@ -9259,9 +9018,9 @@ def assemble(self, user_dict, interview_status=None, old_user_dict=None, force_q
del exc
del tb
raise the_error
- if docassemble.base.functions.this_thread.prevent_going_back:
+ if this_thread.prevent_going_back:
interview_status.can_go_back = False
- docassemble.base.functions.wrap_up()
+ wrap_up()
if self.debug:
interview_status.seeking.append({'done': True, 'time': time.time()})
@@ -9270,7 +9029,7 @@ def load_util(self, the_user_dict):
if not self.consolidated_metadata.get('suppress loading util', False):
exec(import_util, the_user_dict)
- def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **kwargs):
+ def askfor(self, missing_variable, user_dict, old_user_dict, interview_status, **kwargs):
seeking_question = kwargs.get('seeking_question', False)
variable_stack = kwargs.get('variable_stack', set())
questions_tried = kwargs.get('questions_tried', {})
@@ -9281,35 +9040,35 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
follow_mc = kwargs.get('follow_mc', True)
seeking = kwargs.get('seeking', [])
if self.debug:
- seeking.append({'variable': missingVariable, 'time': time.time()})
+ seeking.append({'variable': missing_variable, 'time': time.time()})
if recursion_depth > self.recursion_limit:
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 DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
- # variable_stack.add(missingVariable)
+ # logmessage("askfor: I don't have " + str(missing_variable) + " for language " + str(language))
+ # logmessage("I don't have " + str(missing_variable) + " for language " + str(language))
+ orig_missing_variable = missing_variable
+ set_current_variable(orig_missing_variable)
+ # if missing_variable in variable_stack:
+ # raise DASourceError("Infinite loop: " + missing_variable + " already looked for, where stack is " + str(variable_stack))
+ # variable_stack.add(missing_variable)
# found_generic = False
- # realMissingVariable = missingVariable
+ # realMissingVariable = missing_variable
totry = []
variants = []
level_dict = {}
generic_dict = {}
- expression_as_list = [x for x in match_brackets_or_dot.split(missingVariable) if x != '']
+ expression_as_list = [x for x in match_brackets_or_dot.split(missing_variable) if x != '']
expression_as_list.append('')
recurse_indices(expression_as_list, list_of_indices, [], variants, level_dict, [], generic_dict, [])
# logmessage("variants: " + repr(variants))
for variant in variants:
- totry.append({'real': missingVariable, 'vari': variant, 'iterators': level_dict[variant], 'generic': generic_dict[variant], 'is_generic': 0 if generic_dict[variant] == '' else 1, 'num_dots': variant.count('.'), 'num_iterators': variant.count('[')})
+ totry.append({'real': missing_variable, 'vari': variant, 'iterators': level_dict[variant], 'generic': generic_dict[variant], 'is_generic': 0 if generic_dict[variant] == '' else 1, 'num_dots': variant.count('.'), 'num_iterators': variant.count('[')})
totry = sorted(sorted(sorted(sorted(totry, key=lambda x: len(x['iterators'])), key=lambda x: x['num_iterators'], reverse=True), key=lambda x: x['num_dots'], reverse=True), key=lambda x: x['is_generic'])
# logmessage("ask_for: totry is " + "\n".join([x['vari'] for x in totry]))
questions_to_try = []
for mv in totry:
# realMissingVariable = mv['real']
- missingVariable = mv['vari']
- # logmessage("Trying missingVariable " + missingVariable)
+ missing_variable = mv['vari']
+ # logmessage("Trying missing_variable " + missing_variable)
if mv['is_generic']:
# logmessage("Testing out generic " + mv['generic'])
try:
@@ -9318,22 +9077,22 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
classes_to_look_for = [type(root_evaluated).__name__]
recursive_add_classes(classes_to_look_for, type(root_evaluated))
for generic_object in classes_to_look_for:
- # logmessage("Looking for generic object " + generic_object + " for " + missingVariable)
- if generic_object in self.generic_questions and missingVariable in self.generic_questions[generic_object] and (language in self.generic_questions[generic_object][missingVariable] or '*' in self.generic_questions[generic_object][missingVariable]):
+ # logmessage("Looking for generic object " + generic_object + " for " + missing_variable)
+ if generic_object in self.generic_questions and missing_variable in self.generic_questions[generic_object] and (language in self.generic_questions[generic_object][missing_variable] or '*' in self.generic_questions[generic_object][missing_variable]):
for lang in [language, '*']:
- if lang in self.generic_questions[generic_object][missingVariable]:
- for the_question_to_use in self.sort_with_orderings(self.generic_questions[generic_object][missingVariable][lang]):
- questions_to_try.append((the_question_to_use, True, mv['generic'], mv['iterators'], missingVariable, generic_object))
+ if lang in self.generic_questions[generic_object][missing_variable]:
+ for the_question_to_use in self.sort_with_orderings(self.generic_questions[generic_object][missing_variable][lang]):
+ questions_to_try.append((the_question_to_use, True, mv['generic'], mv['iterators'], missing_variable, generic_object))
except:
pass
continue
# logmessage("askfor: questions to try is " + str(questions_to_try))
- if missingVariable in self.questions:
+ if missing_variable in self.questions:
for lang in [language, '*']:
# logmessage("lang is " + lang)
- if lang in self.questions[missingVariable]:
- for the_question in self.sort_with_orderings(self.questions[missingVariable][lang]):
- questions_to_try.append((the_question, False, 'None', mv['iterators'], missingVariable, None))
+ if lang in self.questions[missing_variable]:
+ for the_question in self.sort_with_orderings(self.questions[missing_variable][lang]):
+ questions_to_try.append((the_question, False, 'None', mv['iterators'], missing_variable, None))
# logmessage("askfor: questions to try is " + str(questions_to_try))
num_cycles = 0
missing_var = "_unknown"
@@ -9342,7 +9101,7 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
if num_cycles > self.loop_limit:
raise DASourceError("Infinite loop detected while looking for " + missing_var)
a_question_was_skipped = False
- docassemble.base.functions.reset_gathering_mode(origMissingVariable)
+ reset_gathering_mode(orig_missing_variable)
# logmessage("Starting the while loop")
try:
for the_question, is_generic, the_x, iterators, missing_var, generic_object in questions_to_try:
@@ -9399,7 +9158,7 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
skip_question = None
for field in question.fields:
if hasattr(field, 'showif_code') and hasattr(field, 'saveas') and field.saveas == field_id:
- docassemble.base.functions.this_thread.misc['current_field'] = field.number
+ this_thread.misc['current_field'] = field.number
result = eval(field.showif_code, user_dict)
if hasattr(field, 'extras') and 'show_if_sign_code' in field.extras and field.extras['show_if_sign_code'] == 0:
if result:
@@ -9429,18 +9188,18 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
gathered = eval(question.gathered, user_dict)
thename = substitute_vars(from_safeid(question.fields[0].saveas), is_generic, the_x, iterators)
if question.use_objects == 'objects':
- user_dict['_DADATA'] = docassemble.base.util.objects_from_data(recursive_eval_dataobject(question.fields[0].data, user_dict), recursive=True, gathered=gathered, name=thename, package=question.package)
+ user_dict['_DADATA'] = objects_from_data(recursive_eval_dataobject(question.fields[0].data, user_dict), recursive=True, gathered=gathered, name=thename, package=question.package)
elif question.use_objects:
- user_dict['_DADATA'] = docassemble.base.util.objects_from_structure(recursive_eval_dataobject(question.fields[0].data, user_dict), root=thename, gathered=gathered)
+ user_dict['_DADATA'] = objects_from_structure(recursive_eval_dataobject(question.fields[0].data, user_dict), root=thename, gathered=gathered)
else:
user_dict['_DADATA'] = recursive_eval_dataobject(question.fields[0].data, user_dict)
the_string = thename + ' = _DADATA'
exec(the_string, user_dict)
del user_dict['_DADATA']
question.post_exec(user_dict)
- docassemble.base.functions.pop_current_variable()
+ pop_current_variable()
question.invalidate_dependencies(user_dict, old_values)
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
if question.question_type == "data_from_code":
question.exec_setup(is_generic, the_x, iterators, user_dict)
old_values = question.get_old_values(user_dict)
@@ -9450,22 +9209,22 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
gathered = eval(question.gathered, user_dict)
thename = substitute_vars(from_safeid(question.fields[0].saveas), is_generic, the_x, iterators)
if question.use_objects == 'objects':
- user_dict['_DADATAFROMCODE'] = docassemble.base.util.objects_from_data(recursive_eval_data_from_code(question.fields[0].data, user_dict), recursive=True, gathered=gathered, name=thename, package=question.package)
+ user_dict['_DADATAFROMCODE'] = objects_from_data(recursive_eval_data_from_code(question.fields[0].data, user_dict), recursive=True, gathered=gathered, name=thename, package=question.package)
elif question.use_objects:
- user_dict['_DADATAFROMCODE'] = docassemble.base.util.objects_from_structure(recursive_eval_data_from_code(question.fields[0].data, user_dict), root=thename, gathered=gathered)
+ user_dict['_DADATAFROMCODE'] = objects_from_structure(recursive_eval_data_from_code(question.fields[0].data, user_dict), root=thename, gathered=gathered)
else:
user_dict['_DADATAFROMCODE'] = recursive_eval_data_from_code(question.fields[0].data, user_dict)
the_string = thename + ' = _DADATAFROMCODE'
exec(the_string, user_dict)
del user_dict['_DADATAFROMCODE']
question.post_exec(user_dict)
- docassemble.base.functions.pop_current_variable()
+ pop_current_variable()
question.invalidate_dependencies(user_dict, old_values)
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
if question.question_type == "objects_from_file":
question.exec_setup(is_generic, the_x, iterators, user_dict)
old_variable = None
- docassemble.base.functions.this_thread.current_question = question
+ this_thread.current_question = question
exec(import_core, user_dict)
if isinstance(question.use_objects, (bool, NoneType)):
use_objects = bool(question.use_objects)
@@ -9500,15 +9259,15 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
pass
continue
question.post_exec(user_dict)
- docassemble.base.functions.pop_current_variable()
+ pop_current_variable()
if old_variable is not None:
question.invalidate_dependencies_of_variable(user_dict, missing_var, old_variable)
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
if question.question_type == "objects":
question.exec_setup(is_generic, the_x, iterators, user_dict)
success = False
old_variable = None
- docassemble.base.functions.this_thread.current_question = question
+ this_thread.current_question = question
for keyvalue in question.objects:
# logmessage("In a for loop for keyvalue")
for raw_variable, object_type_name in keyvalue.items():
@@ -9568,11 +9327,11 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
# question.mark_as_answered(user_dict)
# logmessage("pop current variable")
question.post_exec(user_dict)
- docassemble.base.functions.pop_current_variable()
+ pop_current_variable()
if old_variable is not None:
question.invalidate_dependencies_of_variable(user_dict, missing_var, old_variable)
# logmessage("Returning")
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
if question.question_type == "template":
question.exec_setup(is_generic, the_x, iterators, user_dict)
temp_vars = {}
@@ -9583,13 +9342,13 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
for indexno in range(len(iterators)):
temp_vars[list_of_indices[indexno]] = user_dict[list_of_indices[indexno]]
if question.target is not None:
- return {'type': 'template', 'question_text': question.content.text(user_dict).rstrip(), 'subquestion_text': None, 'continue_label': None, 'audiovideo': None, 'decorations': None, 'help_text': None, 'interview_help_text': None, 'attachments': None, 'question': question, 'selectcompute': {}, 'defaults': {}, 'hints': {}, 'helptexts': {}, 'extras': {}, 'labels': {}, 'sought': missing_var, 'orig_sought': origMissingVariable}
+ return {'type': 'template', 'question_text': question.content.text(user_dict).rstrip(), 'subquestion_text': None, 'continue_label': None, 'audiovideo': None, 'decorations': None, 'help_text': None, 'interview_help_text': None, 'attachments': None, 'question': question, 'selectcompute': {}, 'defaults': {}, 'hints': {}, 'helptexts': {}, 'extras': {}, 'labels': {}, 'sought': missing_var, 'orig_sought': orig_missing_variable}
if question.decorations is None:
decoration_list = []
else:
decoration_list = question.decorations
actual_saveas = substitute_vars(from_safeid(question.fields[0].saveas), is_generic, the_x, iterators)
- # docassemble.base.functions.this_thread.template_vars.append(actual_saveas)
+ # this_thread.template_vars.append(actual_saveas)
found_object = False
try:
the_object = eval(actual_saveas, user_dict)
@@ -9611,8 +9370,8 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
the_object.userdict = user_dict
the_object.tempvars = temp_vars
question.post_exec(user_dict)
- docassemble.base.functions.pop_current_variable()
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ pop_current_variable()
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
if question.question_type == "template_code":
question.exec_setup(is_generic, the_x, iterators, user_dict)
the_filenames = eval(question.compute, user_dict)
@@ -9635,7 +9394,7 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
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)
+ the_filename = package_template_filename(the_filename, package=question.package)
else:
the_filename = None
if the_filename is None or not os.path.isfile(the_filename):
@@ -9675,8 +9434,8 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
the_object.userdict = user_dict
the_object.tempvars = temp_vars
question.post_exec(user_dict)
- docassemble.base.functions.pop_current_variable()
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ pop_current_variable()
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
if question.question_type == "table":
question.exec_setup(is_generic, the_x, iterators, user_dict)
temp_vars = {}
@@ -9702,7 +9461,7 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
table_info.filter_expression = question.fields[0].extras['filter_expression']
table_info.saveas = from_safeid(question.fields[0].saveas)
actual_saveas = substitute_vars(table_info.saveas, is_generic, the_x, iterators)
- # docassemble.base.functions.this_thread.template_vars.append(actual_saveas)
+ # this_thread.template_vars.append(actual_saveas)
the_string = "from docassemble.base.util import DALazyTableTemplate"
exec(the_string, user_dict)
found_object = False
@@ -9723,13 +9482,13 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
the_object.tempvars = temp_vars
# logmessage("Pop variable for table")
question.post_exec(user_dict)
- docassemble.base.functions.pop_current_variable()
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ pop_current_variable()
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
if question.question_type == 'attachments':
question.exec_setup(is_generic, the_x, iterators, user_dict)
old_values = question.get_old_values(user_dict)
- # logmessage("original missing variable is " + origMissingVariable)
- question.processed_attachments(user_dict, seeking_var=origMissingVariable, use_cache=False)
+ # logmessage("original missing variable is " + orig_missing_variable)
+ question.processed_attachments(user_dict, seeking_var=orig_missing_variable, use_cache=False)
if missing_var in variable_stack:
variable_stack.remove(missing_var)
try:
@@ -9739,9 +9498,9 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
logmessage("Problem with attachments block: " + err.__class__.__name__ + ": " + str(err))
continue
question.post_exec(user_dict)
- docassemble.base.functions.pop_current_variable()
+ pop_current_variable()
question.invalidate_dependencies(user_dict, old_values)
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
if question.question_type in ["code", "event_code"]:
question.exec_setup(is_generic, the_x, iterators, user_dict)
was_defined = False
@@ -9753,8 +9512,8 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
except:
pass
if question.question_type == 'event_code':
- docassemble.base.functions.pop_event_stack(origMissingVariable)
- docassemble.base.functions.this_thread.current_question = question
+ pop_event_stack(orig_missing_variable)
+ this_thread.current_question = question
if was_defined:
exec_with_trap(question, user_dict, old_variable=missing_var)
else:
@@ -9763,12 +9522,12 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
if missing_var in variable_stack:
variable_stack.remove(missing_var)
if question.question_type == 'event_code':
- docassemble.base.functions.pop_current_variable()
- docassemble.base.functions.pop_event_stack(origMissingVariable)
+ pop_current_variable()
+ pop_event_stack(orig_missing_variable)
question.invalidate_dependencies(user_dict, old_values)
if was_defined:
exec("del __oldvariable__", user_dict)
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
try:
eval(missing_var, user_dict)
if was_defined:
@@ -9777,9 +9536,9 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
if seeking_question:
continue
# question.mark_as_answered(user_dict)
- docassemble.base.functions.pop_current_variable()
- docassemble.base.functions.pop_event_stack(origMissingVariable)
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ pop_current_variable()
+ pop_event_stack(orig_missing_variable)
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
except:
if was_defined:
try:
@@ -9793,35 +9552,35 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
interview_status.mark_tentative_as_answered(user_dict)
if question.question_type == 'continue':
continue
- return question.ask(user_dict, old_user_dict, the_x, iterators, missing_var, origMissingVariable)
+ return question.ask(user_dict, old_user_dict, the_x, iterators, missing_var, orig_missing_variable)
if a_question_was_skipped:
- raise DASourceError("Infinite loop: " + missingVariable + " already looked for, where stack is " + str(variable_stack))
- if 'forgive_missing_question' in docassemble.base.functions.this_thread.misc and origMissingVariable in docassemble.base.functions.this_thread.misc['forgive_missing_question']:
- docassemble.base.functions.pop_current_variable()
- docassemble.base.functions.pop_event_stack(origMissingVariable)
- if 'action' in docassemble.base.functions.this_thread.current_info and docassemble.base.functions.this_thread.current_info['action'] == origMissingVariable:
- del docassemble.base.functions.this_thread.current_info['action']
- return {'type': 'continue', 'sought': origMissingVariable, 'orig_sought': origMissingVariable}
- if self.options.get('use catchall', False) and not origMissingVariable.endswith('.value'):
+ raise DASourceError("Infinite loop: " + missing_variable + " already looked for, where stack is " + str(variable_stack))
+ if 'forgive_missing_question' in this_thread.misc and orig_missing_variable in this_thread.misc['forgive_missing_question']:
+ pop_current_variable()
+ pop_event_stack(orig_missing_variable)
+ if 'action' in this_thread.current_info and this_thread.current_info['action'] == orig_missing_variable:
+ del this_thread.current_info['action']
+ return {'type': 'continue', 'sought': orig_missing_variable, 'orig_sought': orig_missing_variable}
+ if self.options.get('use catchall', False) and not orig_missing_variable.endswith('.value'):
the_string = "from docassemble.base.util import DACatchAll"
exec(the_string, user_dict)
- the_string = origMissingVariable + ' = DACatchAll(' + repr(origMissingVariable) + ')'
+ the_string = orig_missing_variable + ' = DACatchAll(' + repr(orig_missing_variable) + ')'
exec(the_string, user_dict)
- docassemble.base.functions.pop_current_variable()
- docassemble.base.functions.pop_event_stack(origMissingVariable)
- return {'type': 'continue', 'sought': origMissingVariable, 'orig_sought': origMissingVariable}
- raise DAErrorMissingVariable("Interview has an error. There was a reference to a variable '" + origMissingVariable + "' that could not be looked up in the question file (for language '" + str(language) + "') or in any of the files incorporated by reference into the question file.", variable=origMissingVariable)
+ pop_current_variable()
+ pop_event_stack(orig_missing_variable)
+ return {'type': 'continue', 'sought': orig_missing_variable, 'orig_sought': orig_missing_variable}
+ raise DAErrorMissingVariable("Interview has an error. There was a reference to a variable '" + orig_missing_variable + "' that could not be looked up in the question file (for language '" + str(language) + "') or in any of the files incorporated by reference into the question file.", variable=orig_missing_variable)
except ForcedReRun:
- docassemble.base.functions.pop_current_variable()
- docassemble.base.functions.pop_event_stack(origMissingVariable)
- return {'type': 're_run', 'sought': origMissingVariable, 'orig_sought': origMissingVariable}
+ pop_current_variable()
+ pop_event_stack(orig_missing_variable)
+ return {'type': 're_run', 'sought': orig_missing_variable, 'orig_sought': orig_missing_variable}
except (NameError, UnboundLocalError, DAAttributeError, DAIndexError) as the_exception:
- if 'pending_error' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['pending_error']
+ if 'pending_error' in this_thread.misc:
+ del this_thread.misc['pending_error']
# logmessage("Error in " + the_exception.__class__.__name__ + " is " + str(the_exception))
- if self.debug and docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if self.debug and this_thread.evaluation_context == 'docx':
logmessage("NameError exception during document assembly: " + str(the_exception))
- docassemble.base.functions.reset_context()
+ reset_context()
seeking_question = False
if isinstance(the_exception, ForcedNameError):
# logmessage("askfor: got a ForcedNameError for " + str(the_exception.name))
@@ -9829,7 +9588,7 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
seeking_question = True
# logmessage("Seeking question is True")
exception_name = the_exception.name
- newMissingVariable = exception_name
+ new_missing_variable = exception_name
if the_exception.next_action is not None and not interview_status.checkin:
if 'event_stack' not in user_dict['_internal']:
user_dict['_internal']['event_stack'] = {}
@@ -9848,12 +9607,12 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
if len(new_items) > 0:
user_dict['_internal']['event_stack'][session_uid] = new_items + user_dict['_internal']['event_stack'][session_uid]
if the_exception.arguments is not None:
- docassemble.base.functions.this_thread.current_info.update({'action': exception_name, 'arguments': the_exception.arguments})
+ this_thread.current_info.update({'action': exception_name, 'arguments': the_exception.arguments})
if exception_name.startswith('_da_'):
- docassemble.base.functions.pop_current_variable()
- docassemble.base.functions.pop_event_stack(origMissingVariable)
- return {'type': 're_run', 'sought': origMissingVariable, 'orig_sought': origMissingVariable}
- docassemble.base.functions.this_thread.misc['forgive_missing_question'] = [exception_name]
+ pop_current_variable()
+ pop_event_stack(orig_missing_variable)
+ return {'type': 're_run', 'sought': orig_missing_variable, 'orig_sought': orig_missing_variable}
+ this_thread.misc['forgive_missing_question'] = [exception_name]
else:
# logmessage("regular nameerror")
if type(the_exception) is NameError: # pylint: disable=unidiomatic-typecheck
@@ -9871,171 +9630,171 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
del exc
del tb
follow_mc = True
- newMissingVariable = extract_missing_name(the_exception)
- if newMissingVariable == 'file':
+ new_missing_variable = extract_missing_name(the_exception)
+ if new_missing_variable == 'file':
raise
- # newMissingVariable = str(the_exception).split("'")[1]
- # if newMissingVariable in questions_tried and newMissingVariable in 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()
+ # new_missing_variable = str(the_exception).split("'")[1]
+ # if new_missing_variable in questions_tried and new_missing_variable in variable_stack:
+ # raise DASourceError("Infinite loop: " + missing_variable + " already looked for, where stack is " + str(variable_stack))
+ if new_missing_variable not in questions_tried:
+ questions_tried[new_missing_variable] = set()
else:
- variable_stack.add(missingVariable)
+ variable_stack.add(missing_variable)
if current_question.question_type != 'objects':
- questions_tried[newMissingVariable].add(current_question)
+ questions_tried[new_missing_variable].add(current_question)
try:
- eval(origMissingVariable, user_dict)
+ eval(orig_missing_variable, user_dict)
was_defined = True
except:
was_defined = False
- question_result = self.askfor(newMissingVariable, user_dict, old_user_dict, interview_status, variable_stack=variable_stack, questions_tried=questions_tried, seeking=seeking, follow_mc=follow_mc, recursion_depth=recursion_depth, seeking_question=seeking_question)
- if question_result['type'] == 'continue' and missing_var != newMissingVariable:
+ question_result = self.askfor(new_missing_variable, user_dict, old_user_dict, interview_status, variable_stack=variable_stack, questions_tried=questions_tried, seeking=seeking, follow_mc=follow_mc, recursion_depth=recursion_depth, seeking_question=seeking_question)
+ if question_result['type'] == 'continue' and missing_var != new_missing_variable:
if not was_defined:
try:
- eval(origMissingVariable, user_dict)
+ eval(orig_missing_variable, user_dict)
now_defined = True
except:
now_defined = False
if now_defined:
- docassemble.base.functions.pop_current_variable()
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
- # logmessage("Continuing after asking for newMissingVariable " + str(newMissingVariable))
+ pop_current_variable()
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
+ # logmessage("Continuing after asking for new_missing_variable " + str(new_missing_variable))
continue
- docassemble.base.functions.pop_current_variable()
+ pop_current_variable()
return question_result
except UndefinedError as the_exception:
# logmessage("UndefinedError")
- if self.debug and docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if self.debug and this_thread.evaluation_context == 'docx':
# logmessage(the_exception.__class__.__name__ + " exception during document assembly: " + str(the_exception) + "\n" + traceback.format_exc())
logmessage(the_exception.__class__.__name__ + " exception during document assembly: " + str(the_exception))
- docassemble.base.functions.reset_context()
- newMissingVariable = extract_missing_name(the_exception)
- if newMissingVariable not in questions_tried:
- questions_tried[newMissingVariable] = set()
+ reset_context()
+ new_missing_variable = extract_missing_name(the_exception)
+ if new_missing_variable not in questions_tried:
+ questions_tried[new_missing_variable] = set()
else:
- variable_stack.add(missingVariable)
+ variable_stack.add(missing_variable)
if current_question.question_type != 'objects':
- questions_tried[newMissingVariable].add(current_question)
- question_result = self.askfor(newMissingVariable, user_dict, old_user_dict, interview_status, variable_stack=variable_stack, questions_tried=questions_tried, seeking=seeking, follow_mc=True, recursion_depth=recursion_depth, seeking_question=seeking_question)
+ questions_tried[new_missing_variable].add(current_question)
+ question_result = self.askfor(new_missing_variable, user_dict, old_user_dict, interview_status, variable_stack=variable_stack, questions_tried=questions_tried, seeking=seeking, follow_mc=True, recursion_depth=recursion_depth, seeking_question=seeking_question)
if question_result['type'] == 'continue':
continue
- docassemble.base.functions.pop_current_variable()
+ pop_current_variable()
return question_result
- except CommandError as qError:
- # logmessage("CommandError: " + str(qError))
- docassemble.base.functions.reset_context()
- question_data = {'command': qError.return_type, 'sleep': qError.sleep, 'question': qError.question_text, 'subquestion': qError.subquestion_text}
+ except CommandError as question_error:
+ # logmessage("CommandError: " + str(question_error))
+ reset_context()
+ question_data = {'command': question_error.return_type, 'sleep': question_error.sleep, 'question': question_error.question_text, 'subquestion': question_error.subquestion_text}
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self, new_interview)
new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
new_question.name = "Question_Temp"
- return new_question.ask(user_dict, old_user_dict, 'None', [], missing_var, origMissingVariable)
- except ResponseError as qError:
+ return new_question.ask(user_dict, old_user_dict, 'None', [], missing_var, orig_missing_variable)
+ except ResponseError as question_error:
# logmessage("ResponseError")
- docassemble.base.functions.reset_context()
+ reset_context()
# logmessage("Trapped ResponseError2")
question_data = {'extras': {}}
- if hasattr(qError, 'response') and qError.response is not None:
- question_data['response'] = qError.response
- elif hasattr(qError, 'binaryresponse') and qError.binaryresponse is not None:
- question_data['binaryresponse'] = qError.binaryresponse
- elif hasattr(qError, 'filename') and qError.filename is not None:
- question_data['response filename'] = qError.filename
- elif hasattr(qError, 'url') and qError.url is not None:
- question_data['redirect url'] = qError.url
- elif hasattr(qError, 'all_variables') and qError.all_variables:
- if hasattr(qError, 'include_internal'):
- question_data['include_internal'] = qError.include_internal
+ if hasattr(question_error, 'response') and question_error.response is not None:
+ question_data['response'] = question_error.response
+ elif hasattr(question_error, 'binaryresponse') and question_error.binaryresponse is not None:
+ question_data['binaryresponse'] = question_error.binaryresponse
+ elif hasattr(question_error, 'filename') and question_error.filename is not None:
+ question_data['response filename'] = question_error.filename
+ elif hasattr(question_error, 'url') and question_error.url is not None:
+ question_data['redirect url'] = question_error.url
+ elif hasattr(question_error, 'all_variables') and question_error.all_variables:
+ if hasattr(question_error, 'include_internal'):
+ question_data['include_internal'] = question_error.include_internal
question_data['content type'] = 'application/json'
question_data['all_variables'] = True
- elif hasattr(qError, 'nullresponse') and qError.nullresponse:
- question_data['null response'] = qError.nullresponse
- elif hasattr(qError, 'sleep') and qError.sleep:
- question_data['sleep'] = qError.sleep
- if hasattr(qError, 'content_type') and qError.content_type:
- question_data['content type'] = qError.content_type
- if hasattr(qError, 'response_code') and qError.response_code:
- question_data['response code'] = qError.response_code
+ elif hasattr(question_error, 'nullresponse') and question_error.nullresponse:
+ question_data['null response'] = question_error.nullresponse
+ elif hasattr(question_error, 'sleep') and question_error.sleep:
+ question_data['sleep'] = question_error.sleep
+ if hasattr(question_error, 'content_type') and question_error.content_type:
+ question_data['content type'] = question_error.content_type
+ if hasattr(question_error, 'response_code') and question_error.response_code:
+ question_data['response code'] = question_error.response_code
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self, new_interview)
new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
new_question.name = "Question_Temp"
# the_question = new_question.follow_multiple_choice(user_dict)
- docassemble.base.functions.pop_event_stack(origMissingVariable)
- return new_question.ask(user_dict, old_user_dict, 'None', [], missing_var, origMissingVariable)
- except BackgroundResponseError as qError:
+ pop_event_stack(orig_missing_variable)
+ return new_question.ask(user_dict, old_user_dict, 'None', [], missing_var, orig_missing_variable)
+ except BackgroundResponseError as question_error:
# logmessage("BackgroundResponseError")
- docassemble.base.functions.reset_context()
+ reset_context()
# logmessage("Trapped BackgroundResponseError2")
question_data = {'extras': {}}
- if hasattr(qError, 'backgroundresponse'):
- question_data['backgroundresponse'] = normalize_background_response(qError.backgroundresponse)
- if hasattr(qError, 'sleep'):
- question_data['sleep'] = qError.sleep
+ if hasattr(question_error, 'backgroundresponse'):
+ question_data['backgroundresponse'] = normalize_background_response(question_error.backgroundresponse)
+ if hasattr(question_error, 'sleep'):
+ question_data['sleep'] = question_error.sleep
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self, new_interview)
new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
new_question.name = "Question_Temp"
- docassemble.base.functions.pop_event_stack(origMissingVariable)
- return new_question.ask(user_dict, old_user_dict, 'None', [], missing_var, origMissingVariable)
- except BackgroundResponseActionError as qError:
+ pop_event_stack(orig_missing_variable)
+ return new_question.ask(user_dict, old_user_dict, 'None', [], missing_var, orig_missing_variable)
+ except BackgroundResponseActionError as question_error:
# logmessage("BackgroundResponseActionError")
- docassemble.base.functions.reset_context()
+ reset_context()
# logmessage("Trapped BackgroundResponseActionError2")
question_data = {'extras': {}}
- if hasattr(qError, 'action'):
- question_data['action'] = qError.action
+ if hasattr(question_error, 'action'):
+ question_data['action'] = question_error.action
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self, new_interview)
new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
new_question.name = "Question_Temp"
- docassemble.base.functions.pop_event_stack(origMissingVariable)
- return new_question.ask(user_dict, old_user_dict, 'None', [], missing_var, origMissingVariable)
- except QuestionError as qError:
+ pop_event_stack(orig_missing_variable)
+ return new_question.ask(user_dict, old_user_dict, 'None', [], missing_var, orig_missing_variable)
+ except QuestionError as question_error:
# logmessage("QuestionError")
- docassemble.base.functions.reset_context()
+ reset_context()
# logmessage("Trapped QuestionError")
question_data = {}
- if qError.question:
- question_data['question'] = qError.question
- if qError.subquestion:
- question_data['subquestion'] = qError.subquestion
- if qError.dead_end:
+ if question_error.question:
+ question_data['question'] = question_error.question
+ if question_error.subquestion:
+ question_data['subquestion'] = question_error.subquestion
+ if question_error.dead_end:
pass
- elif qError.buttons:
- question_data['buttons'] = qError.buttons
+ elif question_error.buttons:
+ question_data['buttons'] = question_error.buttons
else:
buttons = []
- if qError.show_exit is not False and not (qError.show_leave is True and qError.show_exit is None):
+ if question_error.show_exit is not False and not (question_error.show_leave is True and question_error.show_exit is None):
exit_button = {word('Exit'): 'exit'}
- if qError.url:
- exit_button.update({'url': qError.url})
+ if question_error.url:
+ exit_button.update({'url': question_error.url})
buttons.append(exit_button)
- if qError.show_leave:
+ if question_error.show_leave:
leave_button = {word('Leave'): 'leave'}
- if qError.url:
- leave_button.update({'url': qError.url})
+ if question_error.url:
+ leave_button.update({'url': question_error.url})
buttons.append(leave_button)
- if qError.show_restart is not False:
+ if question_error.show_restart is not False:
buttons.append({word('Restart'): 'restart'})
if len(buttons) > 0:
question_data['buttons'] = buttons
new_interview_source = InterviewSourceString(content='')
- new_interview = new_interview_source.get_interview()
+ new_interview = Interview(source=new_interview_source)
reproduce_basics(self, new_interview)
new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
new_question.name = "Question_Temp"
new_question.embeds = True
# will this be a problem? yup
the_question = new_question.follow_multiple_choice(user_dict, interview_status, False, 'None', [])
- return the_question.ask(user_dict, old_user_dict, 'None', [], missing_var, origMissingVariable)
+ return the_question.ask(user_dict, old_user_dict, 'None', [], missing_var, orig_missing_variable)
except CodeExecute as code_error:
# logmessage("CodeExecute")
- docassemble.base.functions.reset_context()
+ reset_context()
# if self.debug:
# interview_status.seeking.append({'question': question, 'reason': 'mandatory code'})
# logmessage("Going to execute " + str(code_error.compute) + " where missing_var is " + str(missing_var))
@@ -10045,51 +9804,51 @@ def askfor(self, missingVariable, user_dict, old_user_dict, interview_status, **
code_error.question.mark_as_answered(user_dict)
# logmessage("Got here 1")
# logmessage("returning from running code")
- docassemble.base.functions.pop_current_variable()
+ pop_current_variable()
# logmessage("Got here 2")
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
except:
# raise DASourceError("Problem setting that variable")
continue
- except SyntaxException as qError:
+ except SyntaxException as question_error:
# logmessage("SyntaxException")
- docassemble.base.functions.reset_context()
+ reset_context()
the_question = None
try:
the_question = question
except:
pass
if the_question is not None:
- 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:
+ raise DASourceError(str(question_error) + "\n\n" + str(self.idebug(self.data_for_debug)))
+ raise DASourceError("no question available in askfor: " + str(question_error))
+ except CompileException as question_error:
# logmessage("CompileException")
- docassemble.base.functions.reset_context()
+ reset_context()
the_question = None
try:
the_question = question
except:
pass
if the_question is not None:
- 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:
+ raise DASourceError(str(question_error) + "\n\n" + str(self.idebug(self.data_for_debug)))
+ raise DASourceError("no question available in askfor: " + str(question_error))
+ # except SendFileError as question_error:
# # logmessage("Trapped SendFileError2")
# question_data = {'extras': {}}
- # if hasattr(qError, 'filename') and qError.filename is not None:
- # question_data['response filename'] = qError.filename
- # if hasattr(qError, 'content_type') and qError.content_type:
- # question_data['content type'] = qError.content_type
+ # if hasattr(question_error, 'filename') and question_error.filename is not None:
+ # question_data['response filename'] = question_error.filename
+ # if hasattr(question_error, 'content_type') and question_error.content_type:
+ # question_data['content type'] = question_error.content_type
# new_interview_source = InterviewSourceString(content='')
- # new_interview = new_interview_source.get_interview()
+ # new_interview = Interview(source=new_interview_source)
# new_question = Question(question_data, new_interview, source=new_interview_source, package=self.source.package)
# new_question.name = "Question_Temp"
# return new_question.ask(user_dict, old_user_dict, 'None', [], None, None)
- 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)
- return {'type': 'continue', 'sought': missing_var, 'orig_sought': origMissingVariable}
- raise DAErrorMissingVariable("Interview has an error. There was a reference to a variable '" + origMissingVariable + "' that could not be found in the question file (for language '" + str(language) + "') or in any of the files incorporated by reference into the question file.", variable=origMissingVariable)
+ if 'forgive_missing_question' in this_thread.misc and orig_missing_variable in this_thread.misc['forgive_missing_question']:
+ pop_current_variable()
+ pop_event_stack(orig_missing_variable)
+ return {'type': 'continue', 'sought': missing_var, 'orig_sought': orig_missing_variable}
+ raise DAErrorMissingVariable("Interview has an error. There was a reference to a variable '" + orig_missing_variable + "' that could not be found in the question file (for language '" + str(language) + "') or in any of the files incorporated by reference into the question file.", variable=orig_missing_variable)
def substitute_vars(var, is_generic, the_x, iterators, last_only=False):
@@ -10242,14 +10001,6 @@ def process_selections(data, exclude=None):
return result
-def extract_missing_name(the_error):
- # logmessage("extract_missing_name: string was " + str(string))
- m = nameerror_match.search(str(the_error))
- if m:
- return m.group(1)
- raise the_error
-
-
def auto_determine_type(field_info, the_value=None):
types = {}
if 'selections' in field_info:
@@ -10408,7 +10159,7 @@ def exec_with_trap(the_question, the_dict, old_variable=None):
raise
except:
cl, exc, tb = sys.exc_info()
- exc.user_dict = docassemble.base.functions.serializable_dict(the_dict)
+ exc.user_dict = serializable_dict(the_dict)
if len(traceback.extract_tb(tb)) == 2:
line_with_error = traceback.extract_tb(tb)[-1][1]
if isinstance(line_with_error, int) and line_with_error > 0 and hasattr(the_question, 'sourcecode'):
@@ -10419,8 +10170,8 @@ def exec_with_trap(the_question, the_dict, old_variable=None):
del tb
raise
-ok_outside_string = string.ascii_letters + string.digits + '.[]_'
-ok_inside_string = string.ascii_letters + string.digits + string.punctuation + " "
+OK_OUTSIDE_STRING = string.ascii_letters + string.digits + '.[]_'
+OK_INSIDE_STRING = string.ascii_letters + string.digits + string.punctuation + " "
def parse_var_name(var):
@@ -10464,7 +10215,7 @@ def parse_var_name(var):
the_quote = char
else:
if not (in_quote or in_bracket):
- if char not in ok_outside_string:
+ if char not in OK_OUTSIDE_STRING:
return {'valid': False, 'reason': 'invalid character in variable name'}
if cur_pos == 0:
if char in string.digits or char == '.':
@@ -10473,7 +10224,7 @@ def parse_var_name(var):
if var[cur_pos - 1] == '.' and char in string.digits:
return {'valid': False, 'reason': 'attribute starts with digit'}
if in_quote:
- if char not in ok_inside_string:
+ if char not in OK_INSIDE_STRING:
return {'valid': False, 'reason': 'invalid character in string'}
else:
if char == '.':
@@ -10502,522 +10253,6 @@ def parse_var_name(var):
return {'valid': True, 'objects': objects, 'bracket_objects': bracket_objects, 'final_parts': final_parts}
-class DAExtension(Extension):
-
- def parse(self, parser):
- raise NotImplementedError()
-
- def filter_stream(self, stream):
- # in_var = False
- met_pipe = False
- for token in stream:
- if token.type == 'variable_begin':
- # in_var = True
- met_pipe = False
- if token.type == 'variable_end':
- # in_var = False
- if not met_pipe:
- yield Token(token.lineno, 'pipe', None)
- yield Token(token.lineno, 'name', 'ampersand_filter')
- # if in_var and token.type == 'pipe':
- # met_pipe = True
- yield token
-
-
-class DAEnvironment(Environment):
-
- def from_string(self, source, **kwargs): # pylint: disable=arguments-differ
- source = re.sub(r'({[\%\{].*?[\%\}]})', fix_quotes, source)
- return super().from_string(source, **kwargs)
-
- def getitem(self, obj, argument):
- try:
- return obj[argument]
- except (DAAttributeError, DAIndexError) as err:
- varname = extract_missing_name(err)
- if 'pending_error' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['pending_error']
- return self.undefined(obj=missing, name=varname)
- except (AttributeError, TypeError, LookupError):
- if 'pending_error' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['pending_error']
- return self.undefined(obj=obj, name=argument, accesstype='item')
-
- def getattr(self, obj, attribute):
- try:
- return getattr(obj, attribute)
- except DAAttributeError as err:
- if 'pending_error' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['pending_error']
- varname = extract_missing_name(err)
- return self.undefined(obj=missing, name=varname)
- except AttributeError:
- if 'pending_error' in docassemble.base.functions.this_thread.misc:
- del docassemble.base.functions.this_thread.misc['pending_error']
- return self.undefined(obj=obj, name=attribute, accesstype='attribute')
-
-
-def ampersand_filter(value):
- if value.__class__.__name__ in ('DAFile', 'DALink', 'DAStaticFile', 'DAFileCollection', 'DAFileList'):
- return value
- if value.__class__.__name__ in ('CustomInlineImage', 'InlineImage', 'RichText', 'Listing', 'Document', 'Subdoc', 'DALazyTemplate', 'Markup'):
- return str(value)
- if isinstance(value, (int, bool, float, NoneType)):
- return value
- if not isinstance(value, str):
- value = str(value)
- value = docassemble.base.file_docx.sanitize_xml(value)
- if '' in value or '' in value:
- return re.sub(r'&(?!#?[0-9A-Za-z]+;)', '&', value)
- for auto_filter in docassemble.base.functions.this_thread.misc.get('auto jinja filter', []):
- value = auto_filter(value)
- return re.sub(r'>', '>', re.sub(r'<', '<', re.sub(r'&(?!#?[0-9A-Za-z]+;)', '&', value)))
-
-
-class DAStrictUndefined(StrictUndefined):
- __slots__ = ('_undefined_type',)
-
- def __init__(self, hint=None, obj=missing, name=None, exc=UndefinedError, accesstype=None): # pylint: disable=super-init-not-called
- self._undefined_hint = hint
- self._undefined_obj = obj
- self._undefined_name = name
- self._undefined_exception = exc
- self._undefined_type = accesstype
-
- @internalcode
- def __getattr__(self, name):
- if name[:2] == '__':
- raise AttributeError(name)
- return self._fail_with_undefined_error(attribute=True)
-
- @internalcode
- def __getitem__(self, index):
- if index[:2] == '__':
- raise IndexError(index)
- return self._fail_with_undefined_error(item=True)
-
- @internalcode
- def _fail_with_undefined_error(self, *args, **kwargs):
- if self._undefined_obj is missing:
- hint = "'%s' is undefined" % self._undefined_name
- elif self._undefined_type == 'item' and hasattr(self._undefined_obj, 'instanceName'):
- hint = "'%s[%r]' is undefined" % (
- self._undefined_obj.instanceName,
- self._undefined_name
- )
- elif 'attribute' in kwargs or self._undefined_type == 'attribute':
- if hasattr(self._undefined_obj, 'instanceName'):
- hint = "'%s.%s' is undefined" % (
- self._undefined_obj.instanceName,
- self._undefined_name
- )
- else:
- hint = '%r has no attribute %r' % (
- object_type_repr(self._undefined_obj),
- self._undefined_name
- )
- else:
- if hasattr(self._undefined_obj, 'instanceName'):
- hint = "'%s[%r]' is undefined" % (
- self._undefined_obj.instanceName,
- self._undefined_name
- )
- else:
- hint = '%s has no element %r' % (
- object_type_repr(self._undefined_obj),
- self._undefined_name
- )
- raise self._undefined_exception(hint)
- __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
- __truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \
- __mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \
- __lt__ = __le__ = __gt__ = __ge__ = __int__ = \
- __float__ = __complex__ = __pow__ = __rpow__ = __sub__ = \
- __rsub__ = __iter__ = __str__ = __len__ = __nonzero__ = __eq__ = \
- __ne__ = __bool__ = __hash__ = _fail_with_undefined_error
-
-
-class DASkipUndefined(ChainableUndefined):
- """Undefined handler for Jinja2 exceptions that allows rendering most
- templates that have undefined variables. It will not fix all broken
- templates. For example, if the missing variable is used in a complex
- mathematical expression it may still break (but expressions with only two
- elements should render as '').
- """
-
- def __init__(self, *pargs, **kwargs): # pylint: disable=super-init-not-called
- # Handle the way Docassemble DAEnvironment triggers attribute errors
- pass
-
- def __str__(self) -> str:
- return ''
-
- def __call__(self, *pargs, **kwargs) -> "DASkipUndefined":
- return self
-
- __getitem__ = __getattr__ = __call__
-
- def __eq__(self, *pargs) -> bool:
- return False
-
- # need to return a bool type
- __bool__ = __ne__ = __le__ = __lt__ = __gt__ = __ge__ = __nonzero__ = __eq__
-
- # let undefined variables work in for loops
-
- def __iter__(self, *pargs) -> "DASkipUndefined":
- return self
-
- def __next__(self, *pargs) -> None:
- raise StopIteration
-
- # need to return an int type
-
- def __int__(self, *pargs) -> int:
- return 0
-
- __len__ = __int__
-
- # need to return a float type
-
- def __float__(self, *pargs) -> float:
- return 0.0
-
- # need to return complex type
-
- def __complex__(self, *pargs) -> complex:
- return 0j
-
- def __add__(self, *pargs, **kwargs) -> str:
- return self.__str__()
-
- # type can be anything. we want it to work with `str()` function though
- # and we do not want to silently give wrong math results.
- # note that this means 1 + (undefined) or (undefined) + 1 will work but not 1 + (undefined) + 1
- __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
- __truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \
- __mod__ = __rmod__ = __pos__ = __neg__ = __pow__ = __rpow__ = \
- __sub__ = __rsub__ = __hash__ = __add__
-
-
-def mygetattr(y, attr):
- for attribute in attr.split('.'):
- y = getattr(y, attribute)
- return y
-
-
-def str_or_original(y, case_sensitive):
- if case_sensitive:
- if hasattr(y, 'instanceName'):
- if y.__class__.__name__ in ('Value', 'PeriodicValue'):
- return y.amount()
- return str(y)
- return y
- if hasattr(y, 'instanceName'):
- if y.__class__.__name__ in ('Value', 'PeriodicValue'):
- return y.amount()
- return str(y).lower()
- try:
- return y.lower()
- except:
- return y
-
-
-def dictsort_filter(dictionary, case_sensitive=False, by='key', reverse=False):
- if by == 'value':
- return sorted(dictionary.items(), key=lambda y: str_or_original(y[1], case_sensitive), reverse=reverse)
- return sorted(dictionary.items(), key=lambda y: str_or_original(y[0], case_sensitive), reverse=reverse)
-
-
-def sort_filter(the_array, reverse=False, case_sensitive=False, attribute=None):
- if attribute is None:
- if not case_sensitive:
- def key_func(y):
- return str_or_original(y, case_sensitive)
- else:
- key_func = None
- else:
- if isinstance(attribute, list):
- attributes = [str(y).strip() for y in attribute]
- else:
- attributes = [y.strip() for y in str(attribute).split(',')]
- def key_func(y):
- return [str_or_original(mygetattr(y, attribute), case_sensitive) for attribute in attributes]
- return sorted(the_array, key=key_func, reverse=reverse)
-
-_GroupTuple = namedtuple('_GroupTuple', ['grouper', 'list'])
-_GroupTuple.__repr__ = tuple.__repr__
-_GroupTuple.__str__ = tuple.__str__
-
-
-def groupby_filter(the_array, attr_name):
-
- def func(y):
- return mygetattr(y, attr_name)
- return [_GroupTuple(key, list(values)) for key, values in groupby(sorted(the_array, key=func), func)]
-
-
-def max_filter(the_array, case_sensitive=False, attribute=None):
- it = iter(the_array)
- try:
- first = next(it)
- except StopIteration:
- raise DAError("max: list was empty")
- if attribute:
- def key_func(y):
- return str_or_original(mygetattr(y, attribute), case_sensitive=case_sensitive)
- else:
- def key_func(y):
- return str_or_original(y, case_sensitive=case_sensitive)
- return max(chain([first], it), key=key_func)
-
-
-def min_filter(the_array, case_sensitive=False, attribute=None):
- it = iter(the_array)
- try:
- first = next(it)
- except StopIteration:
- raise DAError("min: list was empty")
- if attribute:
- def key_func(y):
- return str_or_original(mygetattr(y, attribute), case_sensitive=case_sensitive)
- else:
- def key_func(y):
- return str_or_original(y, case_sensitive=case_sensitive)
- return min(chain([first], it), key=key_func)
-
-
-def sum_filter(the_array, attribute=None, start=0):
- if attribute is not None:
- the_array = [mygetattr(y, attribute) for y in the_array]
- return sum(the_array, start)
-
-
-def unique_filter(the_array, case_sensitive=False, attribute=None):
- seen = set()
- if attribute is None:
- for item in the_array:
- new_item = str_or_original(item, case_sensitive)
- if new_item not in seen:
- seen.add(new_item)
- yield item
- else:
- for item in the_array:
- new_item = str_or_original(mygetattr(item, attribute), case_sensitive)
- if new_item not in seen:
- seen.add(new_item)
- yield mygetattr(item, attribute)
-
-
-def join_filter(the_array, d="", attribute=None):
- if attribute is not None:
- return d.join([str(mygetattr(y, attribute)) for y in the_array])
- return d.join([str(y) for y in the_array])
-
-
-def attr_filter(var, attr_name):
- return mygetattr(var, attr_name)
-
-
-def selectattr_filter(*pargs, **kwargs):
- if len(pargs) > 2:
- the_array = pargs[0]
- attr_name = pargs[1]
- func_name = pargs[2]
- env = custom_jinja_env()
- def func(item):
- return env.call_test(func_name, item, pargs[3:], kwargs)
- for item in the_array:
- if func(mygetattr(item, attr_name)):
- yield item
- else:
- for item in pargs[0]:
- if mygetattr(item, pargs[1]):
- yield item
-
-
-def rejectattr_filter(*pargs, **kwargs):
- if len(pargs) > 2:
- the_array = pargs[0]
- attr_name = pargs[1]
- func_name = pargs[2]
- env = custom_jinja_env()
- def func(item):
- return env.call_test(func_name, item, pargs[3:], kwargs)
- for item in the_array:
- if not func(mygetattr(item, attr_name)):
- yield item
- else:
- for item in pargs[0]:
- if not mygetattr(item, pargs[1]):
- yield item
-
-
-def chain_filter(*pargs, **kwargs): # pylint: disable=unused-argument
- the_list = []
- for parg in pargs:
- if isinstance(parg, str):
- the_list.append(parg)
- elif (hasattr(parg, 'instanceName') and hasattr(parg, 'elements')):
- if isinstance(parg.elements, dict):
- for sub_parg in parg.values():
- the_list.append(sub_parg)
- else:
- for sub_parg in parg:
- the_list.append(sub_parg)
- elif isinstance(parg, abc.Iterable):
- for sub_parg in parg:
- the_list.append(sub_parg)
- else:
- the_list.append(parg)
- return chain(*the_list)
-
-
-def map_filter(*pargs, **kwargs):
- if len(pargs) >= 2:
- the_array = pargs[0]
- the_filter = pargs[1]
- env = custom_jinja_env()
- if the_filter not in env.filters:
- raise DAError('filter passed to map() does not exist')
- for item in the_array:
- yield env.call_filter(the_filter, item, pargs[2:], kwargs)
- else:
- if 'attribute' in kwargs:
- if 'default' in kwargs:
- for item in pargs[0]:
- yield mygetattr(item, kwargs['attribute'], kwargs['default'])
- else:
- for item in pargs[0]:
- yield mygetattr(item, kwargs['attribute'])
- elif 'index' in kwargs:
- if 'default' in kwargs:
- for item in pargs[0]:
- yield item.get(kwargs['index'], kwargs['default'])
- else:
- for item in pargs[0]:
- yield item[kwargs['index']]
- elif 'function' in kwargs:
- the_kwargs = kwargs.get('kwargs', {})
- the_pargs = kwargs.get('pargs', [])
- if not isinstance(the_kwargs, dict):
- raise DAError('kwargs passed to map() must be a dictionary')
- if not isinstance(the_pargs, list):
- raise DAError('pargs passed to map() must be a list')
- for item in pargs[0]:
- yield kwargs['function'](item, *the_pargs, **the_kwargs)
- else:
- raise DAError('map() must refer to a function, index, attribute, or filter')
-
-
-def markdown_filter(text):
- return docassemble.base.file_docx.markdown_to_docx(text, docassemble.base.functions.this_thread.current_question, docassemble.base.functions.this_thread.misc.get('docx_template', None))
-
-
-def inline_markdown_filter(text):
- return docassemble.base.file_docx.inline_markdown_to_docx(text, docassemble.base.functions.this_thread.current_question, docassemble.base.functions.this_thread.misc.get('docx_template', None))
-
-
-def get_builtin_jinja_filters():
- return {
- 'ampersand_filter': ampersand_filter,
- 'markdown': markdown_filter,
- 'add_separators': docassemble.base.functions.add_separators,
- 'inline_markdown': inline_markdown_filter,
- 'paragraphs': docassemble.base.functions.single_to_double_newlines,
- 'manual_line_breaks': docassemble.base.functions.manual_line_breaks,
- 'RichText': docassemble.base.file_docx.RichText,
- 'groupby': groupby_filter,
- 'max': max_filter,
- 'min': min_filter,
- 'sum': sum_filter,
- 'unique': unique_filter,
- 'join': join_filter,
- 'attr': attr_filter,
- 'selectattr': selectattr_filter,
- 'rejectattr': rejectattr_filter,
- 'sort': sort_filter,
- 'dictsort': dictsort_filter,
- 'format_date': docassemble.base.util.format_date,
- 'format_datetime': docassemble.base.util.format_datetime,
- 'format_time': docassemble.base.util.format_time,
- 'month_of': docassemble.base.util.month_of,
- 'year_of': docassemble.base.util.year_of,
- 'day_of': docassemble.base.util.day_of,
- 'dow_of': docassemble.base.util.dow_of,
- 'qr_code': docassemble.base.functions.qr_code,
- 'nice_number': docassemble.base.functions.nice_number,
- 'ordinal': docassemble.base.functions.ordinal,
- 'ordinal_number': docassemble.base.functions.ordinal_number,
- 'currency': docassemble.base.functions.currency,
- 'comma_list': docassemble.base.functions.comma_list,
- 'comma_and_list': docassemble.base.functions.comma_and_list,
- 'capitalize': docassemble.base.functions.capitalize,
- 'salutation': docassemble.base.functions.salutation,
- 'alpha': docassemble.base.functions.alpha,
- 'roman': docassemble.base.functions.roman,
- 'word': docassemble.base.functions.word,
- 'bold': docassemble.base.functions.bold,
- 'italic': docassemble.base.functions.italic,
- 'title_case': docassemble.base.functions.title_case,
- 'single_paragraph': docassemble.base.functions.single_paragraph,
- 'phone_number_formatted': docassemble.base.functions.phone_number_formatted,
- 'phone_number_in_e164': docassemble.base.functions.phone_number_in_e164,
- 'country_name': docassemble.base.functions.country_name,
- 'fix_punctuation': docassemble.base.functions.fix_punctuation,
- 'redact': docassemble.base.functions.redact,
- 'verbatim': docassemble.base.functions.verbatim,
- 'map': map_filter,
- 'chain': chain_filter,
- 'any': any,
- 'all': all
- }
-
-
-registered_jinja_filters = {}
-
-
-def custom_jinja_env(skip_undefined=False):
- if skip_undefined:
- env = DAEnvironment(undefined=DASkipUndefined, extensions=[DAExtension])
- else:
- env = DAEnvironment(undefined=DAStrictUndefined, extensions=[DAExtension])
- env.filters.update(registered_jinja_filters)
- env.filters.update(get_builtin_jinja_filters())
- return env
-
-
-def register_jinja_filter(filter_name, func):
- if filter_name in get_builtin_jinja_filters():
- raise DAError("Cannot register filter with same name as built-in filter %s" % filter_name)
- registered_jinja_filters[filter_name] = func
-
-
-def get_docx_variables(the_path):
- names = set()
- if not os.path.isfile(the_path):
- raise DASourceError("Missing docx template file " + os.path.basename(the_path))
- try:
- docx_template = DocxTemplate(the_path)
- docx_template.render_init()
- the_env = custom_jinja_env()
- the_xml = docx_template.get_xml()
- the_xml = re.sub(r'])', r'\n 0:
fields = {}
for field, default, pageno, rect, field_type, export_value in the_fields:
@@ -389,7 +411,8 @@ def fill_template(template, data_strings=None, data_names=None, hidden=None, rea
pdf.save(pdf_file.name)
pdf.close()
if (pdfa or not editable) and len(images) > 0:
- flatten_pdf(pdf_file.name)
+ if use_pdftk:
+ flatten_pdf(pdf_file.name)
if pdfa:
pdf_to_pdfa(pdf_file.name)
if password or owner_password:
diff --git a/docassemble_base/docassemble/base/plugin_manager.py b/docassemble_base/docassemble/base/plugin_manager.py
new file mode 100644
index 000000000..e1510dc4d
--- /dev/null
+++ b/docassemble_base/docassemble/base/plugin_manager.py
@@ -0,0 +1,32 @@
+import pluggy
+from . import hookspecs
+
+
+class DocassemblePluginManager(pluggy.PluginManager):
+ """PluginManager that merges hookimpl kwargnames into argnames on registration.
+
+ pluggy only dispatches params listed in HookImpl.argnames (params without
+ defaults). Params with defaults land in kwargnames and are never filled from
+ caller_kwargs. By merging kwargnames into argnames after each register() call,
+ and having wrapper callers always supply every param, hookimpls can keep their
+ natural Python default-argument signatures.
+ """
+
+ def register(self, plugin, name=None):
+ result = super().register(plugin, name=name)
+ self._merge_kwargnames(plugin)
+ return result
+
+ def _merge_kwargnames(self, plugin):
+ for attr in dir(self.hook):
+ hook_caller = getattr(self.hook, attr, None)
+ if hook_caller is None or not hasattr(hook_caller, 'get_hookimpls'):
+ continue
+ for impl in hook_caller.get_hookimpls():
+ if impl.plugin is plugin and impl.kwargnames:
+ impl.argnames = impl.argnames + impl.kwargnames
+ impl.kwargnames = ()
+
+
+pm = DocassemblePluginManager("docassemble")
+pm.add_hookspecs(hookspecs)
diff --git a/docassemble_base/docassemble/base/py.typed b/docassemble_base/docassemble/base/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/docassemble_base/docassemble/base/read_config.py b/docassemble_base/docassemble/base/read_config.py
index 732082047..c40575f8f 100644
--- a/docassemble_base/docassemble/base/read_config.py
+++ b/docassemble_base/docassemble/base/read_config.py
@@ -9,7 +9,7 @@
try:
total_memory = int(math.ceil(psutil.virtual_memory().total / 1024 ** 3))
except:
- total_memory = 2
+ total_memory = 2 # pylint: disable=invalid-name
import docassemble.base.config
docassemble.base.config.load(arguments=sys.argv)
from docassemble.base.config import daconfig, parse_redis_uri
@@ -179,7 +179,7 @@
days = int(daconfig['backup days'])
assert days >= 0
except:
- days = 14
+ days = 14 # pylint: disable=invalid-name
print('export DABACKUPDAYS="' + str(days) + '"')
else:
print('export DABACKUPDAYS="14"')
@@ -191,6 +191,14 @@
print('export ENABLEUNOCONV=true')
else:
print('export ENABLEUNOCONV=false')
+ if 'enable email server' in daconfig and not daconfig['enable email server']:
+ print('export ENABLEEMAILSERVER=false')
+ else:
+ print('export ENABLEEMAILSERVER=true')
+ if 'enable monitor' in daconfig and not daconfig['enable monitor']:
+ print('export ENABLEMONITOR=false')
+ else:
+ print('export ENABLEMONITOR=true')
if 's3' in daconfig:
s4_options = []
if ('enable' in daconfig['s3'] and daconfig['s3']['enable']) or ('enable' not in daconfig['s3'] and 'bucket' in daconfig['s3'] and daconfig['s3']['bucket'] is not None):
@@ -294,4 +302,6 @@
print('export PIPINDEXURL="' + str(daconfig['pip index url']) + '"')
if 'pip extra index urls' in daconfig and daconfig['pip extra index urls'] is not None and daconfig['pip extra index urls'] != '':
print('export PIPEXTRAINDEXURLS="' + str(daconfig['pip extra index urls']) + '"')
+ if 'pip trusted host' in daconfig and daconfig['pip trusted host'] is not None and daconfig['pip trusted host'] != '':
+ print('export PIPTRUSTEDHOST="github.com ' + str(daconfig['pip trusted host']) + '"')
sys.exit(0)
diff --git a/docassemble_base/docassemble/base/sql.py b/docassemble_base/docassemble/base/sql.py
index 068d1d1df..d8f578b69 100644
--- a/docassemble_base/docassemble/base/sql.py
+++ b/docassemble_base/docassemble/base/sql.py
@@ -1,16 +1,20 @@
import copy
import os
-# import sys
import json
import logging.config
import sqlalchemy
-from docassemble.base.logger import logmessage
-import docassemble.base.functions
-from docassemble.base.functions import server
-from docassemble.base.util import DAList, DAObjectPlusParameters
-from docassemble.base.error import DAAttributeError, DAException
from alembic.config import Config
from alembic import command
+from .error import DAAttributeError, DAException
+from .hooks import (
+ alchemy_url as server_alchemy_url,
+ connect_args as server_connect_args,
+ create_objects_in_db,
+ register_db,
+)
+from .logger import logmessage
+from .thread_context import this_thread
+from .util import DAList, DAObjectPlusParameters
__all__ = ['SQLObject', 'SQLObjectRelationship', 'SQLObjectList', 'SQLRelationshipList', 'StandardRelationshipList', 'alchemy_url', 'connect_args', 'upgrade_db', 'register_db', 'create_objects']
@@ -76,8 +80,8 @@ def __setstate__(self, pickle_dict):
@classmethod
def filter(cls, instance_name, **kwargs):
- if 'dbcache' not in docassemble.base.functions.this_thread.misc:
- docassemble.base.functions.this_thread.misc['dbcache'] = {}
+ if 'dbcache' not in this_thread.misc:
+ this_thread.misc['dbcache'] = {}
listobj = DAList(instance_name, object_type=cls, auto_gather=False)
filters = []
for key, val in kwargs.items():
@@ -85,8 +89,8 @@ def filter(cls, instance_name, **kwargs):
raise DAException("filter: class " + cls.__name__ + " does not have column " + key)
filters.append(getattr(cls._model, key) == val)
for db_entry in list(cls._session.query(cls._model).filter(*filters).order_by(cls._model.id).all()):
- if cls._model.__name__ in docassemble.base.functions.this_thread.misc['dbcache'] and db_entry.id in docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__]:
- listobj.append(docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__][db_entry.id])
+ if cls._model.__name__ in this_thread.misc['dbcache'] and db_entry.id in this_thread.misc['dbcache'][cls._model.__name__]:
+ listobj.append(this_thread.misc['dbcache'][cls._model.__name__][db_entry.id])
else:
obj = listobj.appendObject()
obj.id = db_entry.id
@@ -111,16 +115,16 @@ def any(cls):
@classmethod
def all(cls, instance_name=None):
- if 'dbcache' not in docassemble.base.functions.this_thread.misc:
- docassemble.base.functions.this_thread.misc['dbcache'] = {}
+ if 'dbcache' not in this_thread.misc:
+ this_thread.misc['dbcache'] = {}
if instance_name:
listobj = DAList(instance_name, object_type=cls)
else:
listobj = DAList(object_type=cls)
listobj.set_random_instance_name()
for db_entry in list(cls._session.query(cls._model).order_by(cls._model.id).all()):
- if cls._model.__name__ in docassemble.base.functions.this_thread.misc['dbcache'] and db_entry.id in docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__]:
- listobj.append(docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__][db_entry.id])
+ if cls._model.__name__ in this_thread.misc['dbcache'] and db_entry.id in this_thread.misc['dbcache'][cls._model.__name__]:
+ listobj.append(this_thread.misc['dbcache'][cls._model.__name__][db_entry.id])
else:
obj = listobj.appendObject()
obj.id = db_entry.id
@@ -138,12 +142,12 @@ def all(cls, instance_name=None):
@classmethod
def by_id(cls, the_id, instance_name=None):
- if 'dbcache' not in docassemble.base.functions.this_thread.misc:
- docassemble.base.functions.this_thread.misc['dbcache'] = {}
- if cls._model.__name__ in docassemble.base.functions.this_thread.misc['dbcache'] and the_id in docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__]:
+ if 'dbcache' not in this_thread.misc:
+ this_thread.misc['dbcache'] = {}
+ if cls._model.__name__ in this_thread.misc['dbcache'] and the_id in this_thread.misc['dbcache'][cls._model.__name__]:
if instance_name is None:
- return docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__][the_id]
- obj = docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__][the_id]
+ return this_thread.misc['dbcache'][cls._model.__name__][the_id]
+ obj = this_thread.misc['dbcache'][cls._model.__name__][the_id]
obj.fix_instance_name(obj.instanceName, instance_name)
if instance_name is None:
obj = cls(id=the_id)
@@ -173,8 +177,8 @@ def by_uid(cls, uid, instance_name=None):
def delete_by_id(cls, the_id):
cls._session.query(cls._model).filter(cls._model.id == the_id).delete()
cls._session.commit()
- if 'dbcache' in docassemble.base.functions.this_thread.misc and cls._model.__name__ in docassemble.base.functions.this_thread.misc['dbcache'] and the_id in docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__]:
- docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__][the_id]._zombie = True
+ if 'dbcache' in this_thread.misc and cls._model.__name__ in this_thread.misc['dbcache'] and the_id in this_thread.misc['dbcache'][cls._model.__name__]:
+ this_thread.misc['dbcache'][cls._model.__name__][the_id]._zombie = True
@classmethod
def delete_by_uid(cls, uid):
@@ -185,8 +189,8 @@ def delete_by_uid(cls, uid):
the_id = db_entry.id
cls._session.query(cls._model).filter(getattr(cls._model, cls._uid) == uid).delete()
cls._session.commit()
- if 'dbcache' in docassemble.base.functions.this_thread.misc and cls._model.__name__ in docassemble.base.functions.this_thread.misc['dbcache'] and the_id in docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__]:
- docassemble.base.functions.this_thread.misc['dbcache'][cls._model.__name__][the_id]._zombie = True
+ if 'dbcache' in this_thread.misc and cls._model.__name__ in this_thread.misc['dbcache'] and the_id in this_thread.misc['dbcache'][cls._model.__name__]:
+ this_thread.misc['dbcache'][cls._model.__name__][the_id]._zombie = True
@classmethod
def id_exists(cls, the_id):
@@ -205,25 +209,25 @@ def uid_exists(cls, uid):
return True
def db_from_cache(self, the_id):
- if 'dbcache' not in docassemble.base.functions.this_thread.misc:
- docassemble.base.functions.this_thread.misc['dbcache'] = {}
- if self._model.__name__ not in docassemble.base.functions.this_thread.misc['dbcache']:
- docassemble.base.functions.this_thread.misc['dbcache'][self._model.__name__] = {}
- if the_id in docassemble.base.functions.this_thread.misc['dbcache'][self._model.__name__]:
- return docassemble.base.functions.this_thread.misc['dbcache'][self._model.__name__][the_id]
+ if 'dbcache' not in this_thread.misc:
+ this_thread.misc['dbcache'] = {}
+ if self._model.__name__ not in this_thread.misc['dbcache']:
+ this_thread.misc['dbcache'][self._model.__name__] = {}
+ if the_id in this_thread.misc['dbcache'][self._model.__name__]:
+ return this_thread.misc['dbcache'][self._model.__name__][the_id]
return None
def db_cache(self):
- if 'dbcache' not in docassemble.base.functions.this_thread.misc:
- docassemble.base.functions.this_thread.misc['dbcache'] = {}
- if self._model.__name__ not in docassemble.base.functions.this_thread.misc['dbcache']:
- docassemble.base.functions.this_thread.misc['dbcache'][self._model.__name__] = {}
+ if 'dbcache' not in this_thread.misc:
+ this_thread.misc['dbcache'] = {}
+ if self._model.__name__ not in this_thread.misc['dbcache']:
+ this_thread.misc['dbcache'][self._model.__name__] = {}
if hasattr(self, 'id'):
- docassemble.base.functions.this_thread.misc['dbcache'][self._model.__name__][self.id] = self
+ this_thread.misc['dbcache'][self._model.__name__][self.id] = self
def __del__(self):
- if hasattr(self, 'id') and 'dbcache' in docassemble.base.functions.this_thread.misc and self._model.__name__ in docassemble.base.functions.this_thread.misc['dbcache'] and self.id in docassemble.base.functions.this_thread.misc['dbcache'][self._model.__name__]:
- del docassemble.base.functions.this_thread.misc['dbcache'][self._model.__name__][self.id]
+ if hasattr(self, 'id') and 'dbcache' in this_thread.misc and self._model.__name__ in this_thread.misc['dbcache'] and self.id in this_thread.misc['dbcache'][self._model.__name__]:
+ del this_thread.misc['dbcache'][self._model.__name__][self.id]
def db_delete(self):
self.db_read()
@@ -460,12 +464,12 @@ def del_parent(self, rel_name, parent):
def alchemy_url(db_config):
"""Returns a URL representing a database connection."""
- return server.alchemy_url(db_config)
+ return server_alchemy_url(db_config)
def connect_args(db_config):
"""Returns PostgreSQL arguments for connecting via SSL."""
- return server.connect_args(db_config)
+ return server_connect_args(db_config)
def upgrade_db(url, py_file, engine, version_table=None, name=None, conn_args=None):
@@ -493,23 +497,18 @@ def upgrade_db(url, py_file, engine, version_table=None, name=None, conn_args=No
alembic_cfg.set_main_option("sqlalchemy.url", url)
alembic_cfg.set_main_option("connect_args", json.dumps(conn_args))
alembic_cfg.set_main_option("script_location", alembic_path)
- _real_fileConfig = logging.config.fileConfig
+ real_file_config = logging.config.fileConfig
logging.config.fileConfig = lambda *a, **kw: None
try:
if not sqlalchemy.inspect(engine).has_table(version_table):
command.stamp(alembic_cfg, "head")
command.upgrade(alembic_cfg, "head")
finally:
- logging.config.fileConfig = _real_fileConfig
-
-
-def register_db(db_name):
- db = server.register_db(db_name)
- return db
+ logging.config.fileConfig = real_file_config
def create_objects(filename, db_name):
- url, conn_args, engine = server.create_objects_in_db(db_name)
+ url, conn_args, engine = create_objects_in_db(db_name)
if db_name in upgrades_done:
return
upgrade_db(url, filename, engine, version_table='auto', conn_args=conn_args)
diff --git a/docassemble_base/docassemble/base/standardformatter.py b/docassemble_base/docassemble/base/standardformatter.py
index 1e458f09b..e9263fbaa 100644
--- a/docassemble_base/docassemble/base/standardformatter.py
+++ b/docassemble_base/docassemble/base/standardformatter.py
@@ -7,14 +7,32 @@
import locale
from io import StringIO
from html.parser import HTMLParser
-from docassemble.base.functions import word, get_currency_symbol, comma_and_list, server, custom_types, get_locale
+from docassemble.base.functions import (
+ get_currency_symbol,
+ comma_and_list,
+ custom_types,
+ get_locale,
+)
from docassemble.base.util import format_date, format_datetime, format_time
-# from docassemble.base.generate_key import random_string
-from docassemble.base.filter import markdown_to_html, get_audio_urls, get_video_urls, audio_control, video_control, noquote, to_text, my_escape, process_target, get_icon_html
+from docassemble.base.filter.html import (
+ markdown_to_html,
+ get_audio_urls,
+ get_video_urls,
+ audio_control,
+ video_control,
+ noquote,
+ to_text,
+ my_escape,
+ process_target,
+ get_icon_html,
+)
from docassemble.base.parse import Question
from docassemble.base.logger import logmessage
from docassemble.base.config import daconfig
from docassemble.base.error import DAException
+from .language.words import word
+from .hooks import url_finder, file_finder, generate_csrf
+
equals_byte = bytes('=', 'utf-8')
NoneType = type(None)
@@ -106,7 +124,7 @@ def process_help(help_section, status, full_page=True):
def tracker_tag(status):
output = ''
- output += ' \n'
+ output += ' \n'
# restore this, maybe
# if len(status.next_action):
# output += ' \n'
@@ -150,7 +168,7 @@ def icon_html(status, name, width_value=1.0, width_units='em'):
return icon if icon else ""
if the_image.attribution is not None:
status.attributions.add(the_image.attribution)
- url = server.url_finder(str(the_image.package) + ':' + str(the_image.filename))
+ url = url_finder(str(the_image.package) + ':' + str(the_image.filename))
else:
is_decoration = False
url = name['value']
@@ -158,7 +176,7 @@ def icon_html(status, name, width_value=1.0, width_units='em'):
raise DAException("Could not find filename " + str(the_image.filename) + " for image " + str(name) + " in package " + str(the_image.package))
sizing = 'width:' + str(width_value) + str(width_units) + ';'
if is_decoration:
- filename = server.file_finder(str(the_image.package) + ':' + str(the_image.filename))
+ filename = file_finder(str(the_image.package) + ':' + str(the_image.filename))
if 'extension' in filename and filename['extension'] == 'svg' and 'width' in filename and 'height' in filename:
if filename['width'] and filename['height']:
sizing += 'height:' + str(width_value * (filename['height']/filename['width'])) + str(width_units) + ';'
@@ -246,10 +264,10 @@ def as_sms(status, the_user_dict, links=None, menu_items=None):
qoutput = ''
if status.question.question_type == 'signature':
qoutput += word('Sign Your Name') + "\n"
- # logmessage("The question is " + status.questionText)
- qoutput += to_text(markdown_to_html(status.questionText, trim=False, status=status, strip_newlines=True), terms, links)
- if status.subquestionText:
- qoutput += "\n" + to_text(markdown_to_html(status.subquestionText, status=status), terms, links)
+ # logmessage("The question is " + status.question_text)
+ qoutput += to_text(markdown_to_html(status.question_text, trim=False, status=status, strip_newlines=True), terms, links)
+ if status.subquestion_text:
+ qoutput += "\n" + to_text(markdown_to_html(status.subquestion_text, status=status), terms, links)
# logmessage("output is: " + repr(qoutput))
qoutput += "XXXXMESSAGE_AREAXXXX"
if len(status.question.fields) > 0:
@@ -475,14 +493,14 @@ def as_sms(status, the_user_dict, links=None, menu_items=None):
for (href, label) in links_orig:
if re.search(r'action=', href):
links.append((href, label))
- if len(status.helpText) > 0 or len(terms) > 0 or len(menu_items) > 0:
+ if len(status.help_text) > 0 or len(terms) > 0 or len(menu_items) > 0:
houtput = ''
- for help_section in status.helpText:
+ for help_section in status.help_text:
if houtput != '':
houtput += "\n"
if help_section['heading'] is not None:
houtput += '== ' + to_text(markdown_to_html(help_section['heading'], trim=False, status=status, strip_newlines=True), terms, links) + ' =='
- elif len(status.helpText) > 1:
+ elif len(status.help_text) > 1:
houtput += '== ' + word('Help with this question') + ' =='
houtput += "\n" + to_text(markdown_to_html(help_section['content'], trim=False, status=status, strip_newlines=True), terms, links)
if len(terms) > 0:
@@ -786,7 +804,7 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar
varnames = {}
onchange = []
autocomplete_info = []
- showUnderText = 'underText' in status.extras and len(status.attachments) == 0
+ show_under_text = 'underText' in status.extras and len(status.attachments) == 0
if status.using_navigation == 'vertical':
grid_class = daconfig['grid classes']['vertical navigation']['body']
else:
@@ -804,8 +822,8 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar
status.extra_scripts.append({"type": "custom", "script": strip_script_tags(status.extras['script'])})
if 'css' in status.extras and status.extras['css'] is not None:
status.extra_css.append(status.extras['css'])
- if status.continueLabel:
- continue_label = markdown_to_html(status.continueLabel, trim=True, do_terms=False, status=status)
+ if status.continue_label:
+ continue_label = markdown_to_html(status.continue_label, trim=True, do_terms=False, status=status)
else:
continue_label = word('Continue')
if status.extras.get('hide_continue_button', False):
@@ -828,9 +846,9 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar
back_button += ''
else:
back_button = ''
- if status.question.interview.question_help_button and len(status.helpText):
- if status.helpText[0]['label']:
- help_label = markdown_to_html(status.helpText[0]['label'], trim=True, do_terms=False, status=status)
+ if status.question.interview.question_help_button and len(status.help_text):
+ if status.help_text[0]['label']:
+ help_label = markdown_to_html(status.help_text[0]['label'], trim=True, do_terms=False, status=status)
else:
help_label = status.question.help()
help_button = '\n '
@@ -838,7 +856,7 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar
help_button_area = ''
else:
help_button_area = ''
- for help_section in status.helpText:
+ for help_section in status.help_text:
help_button_area += process_help(help_section, status, full_page=False)
if status.question.question_type == "signature":
help_button_area += ''
@@ -902,11 +920,11 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar
if 'image' in decoration:
the_image = status.question.interview.images.get(decoration['image'], None)
if the_image is not None:
- url = server.url_finder(str(the_image.package) + ':' + str(the_image.filename))
+ url = url_finder(str(the_image.package) + ':' + str(the_image.filename))
width_value = DECORATION_SIZE
width_units = DECORATION_UNITS
sizing = 'width:' + str(width_value) + str(width_units) + ';'
- filename = server.file_finder(str(the_image.package) + ':' + str(the_image.filename))
+ filename = file_finder(str(the_image.package) + ':' + str(the_image.filename))
if 'extension' in filename and filename['extension'] == 'svg' and 'width' in filename:
if filename['width'] and filename['height']:
sizing += 'height:' + str(width_value * (filename['height']/filename['width'])) + str(width_units) + ';'
@@ -950,22 +968,22 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar
# else:
back_clear_button = '' + word('Clear') + ''
output += ' \n \n \n \n ' + back_clear_button + '\n ' + continue_label + '\n '
- if status.questionText:
- output += markdown_to_html(status.questionText, trim=True, status=status)
+ if status.question_text:
+ output += markdown_to_html(status.question_text, trim=True, status=status)
else:
output += word('Sign Your Name')
output += '\n \n \n \n \n \n'
if status.pre:
output += ' ' + markdown_to_html(status.pre, trim=False, status=status) + '\n'
- if status.questionText:
- output += ' ' + decoration_text + markdown_to_html(status.questionText, trim=True, status=status, strip_newlines=True) + '
\n'
+ if status.question_text:
+ output += ' ' + decoration_text + markdown_to_html(status.question_text, trim=True, status=status, strip_newlines=True) + '
\n'
output += ' '
- if status.subquestionText:
- output += ' \n' + markdown_to_html(status.subquestionText, status=status) + ' \n'
+ if status.subquestion_text:
+ output += ' \n' + markdown_to_html(status.subquestion_text, status=status) + ' \n'
else:
output += '\n '
output += '\n \n \n '
- if showUnderText:
+ if show_under_text:
output += ' ' + markdown_to_html(status.extras['underText'], trim=False, status=status) + '\n ' + markdown_to_html(status.extras['underText'], trim=True, status=status) + ''
output += "\n "
if status.submit:
@@ -993,9 +1011,9 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar
datatypes[status.question.fields[0].saveas] = status.question.fields[0].datatype
output += status.pre
output += indent_by(audio_text, 12) + ' \n')
output += status.pre
output += indent_by(audio_text, 12) + '
@@ -2466,7 +2484,7 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar
output += """
\n '
output += """
-
+
@@ -2544,13 +2562,13 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar
"""
- if len(status.interviewHelpText) > 0 or (len(status.helpText) > 0 and not status.question.interview.question_help_button):
+ if len(status.interview_help_text) > 0 or (len(status.help_text) > 0 and not status.question.interview.question_help_button):
if status.using_screen_reader and 'help' in status.screen_reader_links:
output += ' \n' + indent_by(audio_control(status.screen_reader_links['help'], preload="none", title_text=word('Read this screen out loud')), 14) + ' \n'
if status.question.interview.question_help_button:
- help_parts = status.interviewHelpText
+ help_parts = status.interview_help_text
else:
- help_parts = status.helpText + status.interviewHelpText
+ help_parts = status.help_text + status.interview_help_text
for help_section in help_parts:
output += process_help(help_section, status)
# if len(status.attributions):
@@ -3472,7 +3490,7 @@ def input_for(status, field, embedded=False, floating_label=None):
else:
defaultstring = ''
if is_hidden and not is_disabled:
- return ''
+ return ''
input_type = field.datatype
if field.datatype == 'datetime':
input_type = 'datetime-local'
diff --git a/docassemble_base/docassemble/base/thread_context.py b/docassemble_base/docassemble/base/thread_context.py
new file mode 100644
index 000000000..c8ea6116d
--- /dev/null
+++ b/docassemble_base/docassemble/base/thread_context.py
@@ -0,0 +1,157 @@
+import copy
+import contextvars
+from contextlib import contextmanager
+from typing import Any, Dict
+from types import SimpleNamespace
+from werkzeug.local import LocalProxy
+import markdown
+from .hooks import (
+ get_default_language,
+ get_default_dialect,
+ get_default_voice,
+ get_default_country,
+ get_default_locale,
+)
+
+_current_user_dict: contextvars.ContextVar[Dict[str, Any]] = contextvars.ContextVar(
+ "current_user_dict"
+)
+_old_user_dict: contextvars.ContextVar[Dict[str, Any]] = contextvars.ContextVar(
+ "old_user_dict"
+)
+_current_globals: contextvars.ContextVar[SimpleNamespace] = contextvars.ContextVar(
+ "current_globals"
+)
+
+class GenericObject:
+
+ def __init__(self):
+ self.user = None
+ self.role = 'user'
+
+
+def empty_globals() -> SimpleNamespace:
+ global_obj = SimpleNamespace()
+ global_obj.language = get_default_language()
+ global_obj.dialect = get_default_dialect()
+ global_obj.voice = get_default_voice()
+ global_obj.country = get_default_country()
+ global_obj.locale = get_default_locale()
+ global_obj.current_info = {}
+ global_obj.internal = {}
+ global_obj.initialized = False
+ global_obj.session_id = None
+ global_obj.current_package = None
+ global_obj.interview = None
+ global_obj.interview_status = None
+ global_obj.evaluation_context = None
+ global_obj.gathering_mode = {}
+ global_obj.global_vars = GenericObject()
+ global_obj.current_variable = []
+ global_obj.open_files = set()
+ global_obj.markdown = markdown.Markdown(extensions=['smarty', 'markdown.extensions.sane_lists', 'markdown.extensions.tables', 'markdown.extensions.attr_list', 'markdown.extensions.md_in_html', 'footnotes'], output_format='html5')
+ global_obj.saved_files = {}
+ global_obj.message_log = []
+ global_obj.misc = {}
+ global_obj.probing = False
+ global_obj.prevent_going_back = False
+ global_obj.current_question = None
+ global_obj.current_section = None
+ return global_obj
+
+def copy_of_globals(original: SimpleNamespace) -> SimpleNamespace:
+ global_obj = SimpleNamespace()
+ global_obj.language = original.language
+ global_obj.dialect = original.dialect
+ global_obj.voice = original.voice
+ global_obj.country = original.country
+ global_obj.locale = original.locale
+ global_obj.current_info = copy.deepcopy(original.current_info)
+ global_obj.internal = {}
+ global_obj.initialized = original.initialized
+ global_obj.session_id = original.session_id
+ global_obj.current_package = original.current_package
+ global_obj.interview = original.interview
+ global_obj.interview_status = original.interview_status
+ global_obj.evaluation_context = None
+ global_obj.gathering_mode = {}
+ global_obj.global_vars = GenericObject()
+ global_obj.current_variable = original.current_variable
+ global_obj.open_files = original.open_files
+ global_obj.markdown = original.markdown
+ global_obj.saved_files = {}
+ global_obj.message_log = []
+ global_obj.misc = copy.deepcopy({k: v for k, v in original.misc.items() if not (k.startswith('yaml_') or k in ('pending_error', 'docx_subdocs', 'docx_include_count', 'docx_template', 'dbcache'))})
+ global_obj.probing = False
+ global_obj.prevent_going_back = False
+ global_obj.current_question = None
+ global_obj.current_section = None
+ return global_obj
+
+# def backup_thread_variables():
+# reset_context()
+# for key in ('pending_error', 'docx_subdocs', 'dbcache'):
+# if key in this_thread.misc:
+# del this_thread.misc[key]
+# backup = {}
+# for key in ('interview', 'interview_status', 'open_files', 'current_question'):
+# if hasattr(this_thread, key):
+# backup[key] = getattr(this_thread, key)
+# for key in ['language', 'dialect', 'country', 'locale', 'current_info', 'internal', 'initialized', 'session_id', 'current_package', 'interview', 'interview_status', 'evaluation_context', 'gathering_mode', 'global_vars', 'current_variable', 'saved_files', 'message_log', 'misc', 'probing', 'prevent_going_back', 'current_question']:
+# if hasattr(this_thread, key):
+# backup[key] = getattr(this_thread, key)
+# if key == 'global_vars':
+# this_thread.global_vars = GenericObject()
+# elif key == 'misc':
+# for key in [item for item in this_thread.misc.keys() if item.startswith('yaml_')]:
+# del this_thread.misc[key]
+# setattr(this_thread, key, copy.deepcopy(this_thread.misc))
+# elif key == 'current_info':
+# setattr(this_thread, key, copy.deepcopy(getattr(this_thread, key)))
+# elif key in ('internal', 'gathering_mode', 'saved_files'):
+# setattr(this_thread, key, {})
+# elif key in ('current_variable', 'message_log'):
+# setattr(this_thread, key, [])
+# return backup
+
+
+@contextmanager
+def user_dict_context(user_dict: Dict[str, Any]):
+ token = _current_user_dict.set(user_dict)
+ try:
+ yield user_dict
+ finally:
+ _current_user_dict.reset(token)
+
+
+def get_current_user_dict() -> Dict[str, Any] | None:
+ return _current_user_dict.get(None)
+
+
+@contextmanager
+def old_user_dict_context(user_dict: Dict[str, Any]):
+ token = _old_user_dict.set(user_dict)
+ try:
+ yield user_dict
+ finally:
+ _old_user_dict.reset(token)
+
+
+def get_old_user_dict() -> Dict[str, Any] | None:
+ return _old_user_dict.get(None)
+
+
+@contextmanager
+def global_context(context_vars: SimpleNamespace):
+ token = _current_globals.set(context_vars)
+ try:
+ yield context_vars
+ finally:
+ _current_globals.reset(token)
+
+
+def get_globals() -> SimpleNamespace:
+ return _current_globals.get(None)
+
+
+this_thread: SimpleNamespace = LocalProxy(lambda: _current_globals.get(None))
diff --git a/docassemble_base/docassemble/base/util.py b/docassemble_base/docassemble/base/util.py
index 7e636dc68..39e4af65d 100644
--- a/docassemble_base/docassemble/base/util.py
+++ b/docassemble_base/docassemble/base/util.py
@@ -1,3 +1,5 @@
+# ruff: noqa: F401
+# pylint: disable=unused-import
from collections import OrderedDict, abc
from decimal import Decimal
from functools import reduce
@@ -32,7 +34,7 @@
try:
import zoneinfo
except ImportError:
- from backports import zoneinfo
+ from backports import zoneinfo # type: ignore[no-redef]
from PIL import Image, ImageEnhance
from twilio.rest import Client as TwilioRestClient
import pycountry
@@ -40,35 +42,368 @@
from jinja2.exceptions import TemplateError
import dateutil
import dateutil.parser
-import babel.dates
-# import redis
import phonenumbers
from bs4 import BeautifulSoup
import i18naddress
from pyzbar.pyzbar import decode
from docxtpl import InlineImage, Subdoc, DocxTemplate
-# import tablib
from docx import Document
from pikepdf import Pdf
+import google_auth_httplib2
import google.cloud
-from docassemble.base.config import in_celery, daconfig
-from docassemble.base.error import DAError, DAValidationError, DAIndexError, DAWebError, LazyNameError, DAAttributeError, DAException
-from docassemble.base.file_docx import include_docx_template
-from docassemble.base.filter import markdown_to_html
-from docassemble.base.functions import alpha, roman, item_label, comma_and_list, get_language, set_language, get_dialect, get_voice, set_country, get_country, word, comma_list, ordinal, ordinal_number, need, nice_number, quantity_noun, possessify, verb_past, verb_present, noun_plural, noun_singular, space_to_underscore, force_ask, force_gather, period_list, name_suffix, currency_symbol, currency, indefinite_article, nodoublequote, capitalize, title_case, url_of, do_you, did_you, does_a_b, did_a_b, were_you, was_a_b, have_you, has_a_b, your, her, his, their, is_word, get_locale, set_locale, update_locale, process_action, url_action, get_info, set_info, get_config, prevent_going_back, qr_code, action_menu_item, from_b64_json, defined, define, value, message, response, json_response, command, single_paragraph, quote_paragraphs, location_returned, location_known, user_lat_lon, interview_url, interview_url_action, interview_url_as_qr, interview_url_action_as_qr, interview_email, get_emails, static_image, action_arguments, action_argument, language_functions, language_function_constructor, get_default_timezone, user_logged_in, interface, user_privileges, user_has_privilege, user_info, current_context, background_action, background_response, background_response_action, background_error_action, us, set_live_help_status, chat_partners_available, phone_number_in_e164, phone_number_formatted, phone_number_is_valid, countries_list, country_name, write_record, read_records, delete_record, variables_as_json, all_variables, server, language_from_browser, device, plain, bold, italic, states_list, state_name, subdivision_type, indent, raw, fix_punctuation, set_progress, get_progress, referring_url, undefine, invalidate, dispatch, yesno, noyes, split, showif, showifdef, phone_number_part, set_parts, log, encode_name, decode_name, interview_list, interview_menu, server_capabilities, session_tags, get_chat_log, get_user_list, get_user_info, set_user_info, get_user_secret, create_user, invite_user, create_session, get_session_variables, set_session_variables, get_question_data, go_back_in_session, manage_privileges, salutation, redact, ensure_definition, forget_result_of, re_run_logic, reconsider, set_title, set_save_status, single_to_double_newlines, CustomDataType, verbatim, add_separators, update_ordinal_numbers, update_ordinal_function, update_language_function, update_nice_numbers, update_word_collection, store_variables_snapshot, get_uid, update_terms, possessify_long, a_in_the_b, its, the, this, these, underscore_to_space, some, set_variables, language_name, run_action_in_session # noqa: F401 # pylint: disable=unused-import
-from docassemble.base.generate_key import random_alphanumeric, random_string
-from docassemble.base.logger import logmessage
-from docassemble.base.pandoc import word_to_markdown, concatenate_files, can_convert_word_to_markdown
-import docassemble.base.file_docx
-import docassemble.base.filter
-import docassemble.base.functions
-import docassemble.base.geocode
import docassemble.base.pandoc
-import docassemble.base.parse
-import docassemble.base.pdftk
-from docassemble.base import DA
-from docassemble.webapp.da_flask_mail import Message
-import google_auth_httplib2
+from . import DA
+from .config import in_celery, daconfig
+from .dates import (
+ today,
+ babel_language,
+ month_of,
+ day_of,
+ dow_of,
+ year_of,
+ format_date,
+ format_datetime,
+ format_time,
+ DateTimeDelta,
+ DADateTime,
+ current_datetime,
+ as_datetime,
+ dd,
+ dt,
+ date_interval,
+ date_difference,
+)
+from .empty import DAEmpty
+from .error import (
+ DAError,
+ DAValidationError,
+ DAIndexError,
+ DAWebError,
+ LazyNameError,
+ DAAttributeError,
+ DAException,
+)
+from .filter.docx import include_docx_template, markdown_to_docx
+from .filter.html import markdown_to_html
+from .functions import (
+ alpha,
+ roman,
+ item_label,
+ comma_and_list,
+ get_language,
+ set_language,
+ get_dialect,
+ get_voice,
+ set_country,
+ get_country,
+ word,
+ comma_list,
+ ordinal,
+ ordinal_number,
+ need,
+ nice_number,
+ quantity_noun,
+ possessify,
+ verb_past,
+ verb_present,
+ noun_plural,
+ noun_singular,
+ space_to_underscore,
+ force_ask,
+ force_gather,
+ period_list,
+ name_suffix,
+ currency_symbol,
+ currency,
+ indefinite_article,
+ nodoublequote,
+ capitalize,
+ title_case,
+ url_of,
+ do_you,
+ did_you,
+ does_a_b,
+ did_a_b,
+ were_you,
+ was_a_b,
+ have_you,
+ has_a_b,
+ your,
+ her,
+ his,
+ their,
+ is_word,
+ get_locale,
+ set_locale,
+ update_locale,
+ process_action,
+ url_action,
+ get_info,
+ set_info,
+ get_config,
+ prevent_going_back,
+ qr_code,
+ action_menu_item,
+ from_b64_json,
+ defined,
+ define,
+ value,
+ message,
+ response,
+ json_response,
+ command,
+ single_paragraph,
+ quote_paragraphs,
+ location_returned,
+ location_known,
+ user_lat_lon,
+ interview_url,
+ interview_url_action,
+ interview_url_as_qr,
+ interview_url_action_as_qr,
+ interview_email,
+ get_emails,
+ static_image,
+ action_arguments,
+ action_argument,
+ language_functions,
+ language_function_constructor,
+ get_default_timezone,
+ user_logged_in,
+ interface,
+ user_privileges,
+ user_has_privilege,
+ user_info,
+ current_context,
+ background_action,
+ background_response,
+ background_response_action,
+ background_error_action,
+ us,
+ set_live_help_status,
+ chat_partners_available,
+ phone_number_in_e164,
+ phone_number_formatted,
+ phone_number_is_valid,
+ countries_list,
+ country_name,
+ write_record,
+ read_records,
+ delete_record,
+ variables_as_json,
+ all_variables,
+ language_from_browser,
+ device,
+ plain,
+ bold,
+ italic,
+ states_list,
+ state_name,
+ subdivision_type,
+ indent,
+ raw,
+ fix_punctuation,
+ set_progress,
+ get_progress,
+ referring_url,
+ undefine,
+ invalidate,
+ dispatch,
+ yesno,
+ noyes,
+ split,
+ showif,
+ showifdef,
+ phone_number_part,
+ set_parts,
+ log,
+ encode_name,
+ decode_name,
+ interview_list,
+ interview_menu,
+ server_capabilities,
+ session_tags,
+ get_chat_log,
+ get_user_list,
+ get_user_info,
+ set_user_info,
+ get_user_secret,
+ create_user,
+ invite_user,
+ create_session,
+ get_session_variables,
+ set_session_variables,
+ get_question_data,
+ go_back_in_session,
+ manage_privileges,
+ salutation,
+ redact,
+ ensure_definition,
+ forget_result_of,
+ re_run_logic,
+ reconsider,
+ set_title,
+ set_save_status,
+ single_to_double_newlines,
+ CustomDataType,
+ verbatim,
+ add_separators,
+ update_ordinal_numbers,
+ update_ordinal_function,
+ update_language_function,
+ update_nice_numbers,
+ update_word_collection,
+ store_variables_snapshot,
+ get_uid,
+ update_terms,
+ possessify_long,
+ a_in_the_b,
+ its,
+ the,
+ this,
+ these,
+ underscore_to_space,
+ some,
+ set_variables,
+ language_name,
+ run_action_in_session,
+)
+from .functions import (
+ get_user_dict,
+ get_action_stack,
+ safe_json,
+ DALocalFile,
+ set_gathering_mode,
+ get_gathering_mode,
+ set_context,
+ reset_context,
+)
+from .generate_key import random_alphanumeric, random_string
+from .geocode import GoogleV3GeoCoder, AzureMapsGeoCoder
+from .hooks import (
+ chord,
+ cloud_custom,
+ fg_make_png_for_pdf,
+ file_finder,
+ file_number_finder,
+ file_privilege_access,
+ file_set_attributes,
+ file_user_access,
+ fix_pickle_obj,
+ get_button_class_prefix,
+ get_celery_app,
+ get_cloud,
+ get_configuration,
+ get_ext_and_mimetype,
+ get_mail_class,
+ get_new_file_number,
+ get_saved_file_class,
+ get_server_redis,
+ get_server_redis_user,
+ get_sms_session as server_get_sms_session,
+ get_twilio_config,
+ get_user_object,
+ google_api,
+ initiate_sms_session as server_initiate_sms_session,
+ make_png_for_pdf as server_make_png_for_pdf,
+ ocr_google_in_background,
+ path_from_reference,
+ retrieve_stashed_data as server_retrieve_stashed_data,
+ secure_filename,
+ secure_filename_spaces_ok,
+ secure_filename_unicode_ok,
+ send_fax as server_send_fax,
+ send_mail,
+ server_sql_defined,
+ server_sql_delete,
+ server_sql_get,
+ server_sql_keys,
+ server_sql_set,
+ sms_body,
+ stash_data as server_stash_data,
+ task_ready,
+ terminate_sms_session as server_terminate_sms_session,
+ url_finder,
+ variables_snapshot_connect,
+ variables_snapshot_connection,
+ wait_for_task,
+)
+from .jinja import (
+ custom_jinja_env,
+ fix_quotes,
+ get_docx_variables,
+ register_jinja_filter as jinja_register_jinja_filter,
+)
+from .language.language import (
+ am_i,
+ are_we,
+ are_word,
+ are_you,
+ are_you_plural,
+ did_a_b_plural,
+ did_i,
+ did_we,
+ did_you_plural,
+ do_a_b,
+ do_i,
+ do_we,
+ do_you_plural,
+ genderless_objective,
+ genderless_self,
+ genderless_subjective,
+ have_a_b,
+ have_i,
+ have_we,
+ have_you_plural,
+ he_subjective,
+ her_objective,
+ herself,
+ him_objective,
+ himself,
+ i_subjective,
+ it_objective,
+ it_subjective,
+ itself,
+ me_objective,
+ my_possessive,
+ myself,
+ our_objective,
+ our_possessive,
+ ourselves,
+ she_subjective,
+ them_objective,
+ themselves,
+ they_subjective,
+ us_objective,
+ was_i,
+ we_subjective,
+ were_a_b_plural,
+ were_we,
+ were_you_plural,
+ you_objective,
+ you_objective_plural,
+ you_subjective,
+ you_subjective_plural,
+ your_plural,
+ yourself,
+ yourselves,
+)
+from .logger import logmessage
+from .pandoc import (
+ can_convert_word_to_markdown,
+ concatenate_files,
+ convert_file,
+ word_to_markdown,
+ word_to_pdf,
+)
+from .pdftk import (
+ apply_qpdf,
+ extract_pages,
+ overlay_pdf as pdftk_overlay_pdf,
+ overlay_pdf_multi,
+ read_fields,
+)
+from .thread_context import this_thread
capitalize_func = capitalize
NoneType = type(None)
@@ -83,7 +418,6 @@
TESSERACT_PATH = 'tesseract'
if daconfig.get('tesseract with celery', False):
TESSERACT_MODE = REMOTE
- from docassemble.tesseract.tasks import run_tesseract, run_gs # pylint: disable=import-error,no-name-in-module,ungrouped-imports
elif TESSERACT_PATH and shutil.which(TESSERACT_PATH):
TESSERACT_MODE = LOCAL
else:
@@ -422,210 +756,8 @@ def get_unique_name():
# return newname
-class DAEmpty:
- """An object that silently absorbs any attribute access or operation.
-
- DAEmpty avoids triggering errors about missing information by returning
- another DAEmpty for any attribute access, returning empty values for
- string conversion and length, and absorbing arithmetic operations.
-
- Attributes:
- str (str): The string value returned when the object is converted to
- text. Defaults to the empty string.
- """
-
- def __init__(self, *pargs, **kwargs): # pylint: disable=unused-argument
- self.str = str(kwargs.get('str', ''))
-
- def __getattr__(self, thename):
- if thename.startswith('__') or thename == 'str':
- return object.__getattribute__(self, thename)
- return DAEmpty()
-
- def __str__(self):
- try:
- return object.__getattribute__(self, 'str')
- except:
- return ''
-
- def __dir__(self):
- return []
-
- def __contains__(self, item):
- return False
-
- def __iter__(self):
- the_list = []
- return the_list.__iter__()
-
- def __len__(self):
- return 0
-
- def __reversed__(self):
- return []
-
- def __getitem__(self, index):
- return DAEmpty()
-
- def __setitem__(self, index, val):
- pass
-
- def __delitem__(self, index):
- pass
-
- def __call__(self, *pargs, **kwargs):
- return DAEmpty()
-
- def __repr__(self):
- return repr('')
-
- def __add__(self, other):
- return other
-
- def __sub__(self, other):
- return other
-
- def __mul__(self, other):
- return other
-
- def __floordiv__(self, other):
- return other
-
- def __mod__(self, other):
- return other
-
- def __divmod__(self, other):
- return other
-
- def __pow__(self, other):
- return other
-
- def __lshift__(self, other):
- return other
-
- def __rshift__(self, other):
- return other
-
- def __and__(self, other):
- return other
-
- def __xor__(self, other):
- return other
-
- def __or__(self, other):
- return other
-
- def __div__(self, other):
- return other
-
- def __truediv__(self, other):
- return other
-
- def __radd__(self, other):
- return other
-
- def __rsub__(self, other):
- return other
-
- def __rmul__(self, other):
- return other
-
- def __rdiv__(self, other):
- return other
-
- def __rtruediv__(self, other):
- return other
-
- def __rfloordiv__(self, other):
- return other
-
- def __rmod__(self, other):
- return other
-
- def __rdivmod__(self, other):
- return other
-
- def __rpow__(self, other):
- return other
-
- def __rlshift__(self, other):
- return other
-
- def __rrshift__(self, other):
- return other
-
- def __rand__(self, other):
- return other
-
- def __ror__(self, other):
- return other
-
- def __neg__(self):
- return 0
-
- def __pos__(self):
- return 0
-
- def __abs__(self):
- return 0
-
- def __invert__(self):
- return 0
-
- def __complex__(self):
- return 0
-
- def __int__(self):
- return int(0)
-
- def __float__(self):
- return float(0)
-
- def __oct__(self):
- return oct(0)
-
- def __hex__(self):
- return hex(0)
-
- def __index__(self):
- return int(0)
-
- def __le__(self, other):
- return True
-
- def __ge__(self, other):
- return self is other or False
-
- def __gt__(self, other):
- return False
-
- def __lt__(self, other):
- return True
-
- def __eq__(self, other):
- return self is other
-
- def __ne__(self, other):
- return self is not other
-
- def __hash__(self):
- return hash(('',))
-
- def as_dict(self):
- return self.to_json()
-
- def to_json(self):
- output = {'_class': 'docassemble.base.util.DAEmpty'}
- try:
- output.update({'str': object.__getattribute__(self, 'str')})
- except Exception:
- pass
- return output
-
-
class DAObjectPlusParameters:
"""A wrapper pairing a DAObject with initialization parameters for lazy object construction."""
- pass
class DAObject:
@@ -723,8 +855,8 @@ def __init__(self, *pargs, **kwargs):
thename = get_unique_name()
self.has_nonrandom_instance_name = False
del frame
- self.instanceName = str(thename)
- self.attrList = []
+ self.instanceName = str(thename) # pylint: disable=invalid-name
+ self.attrList = [] # pylint: disable=invalid-name
self.init(*pargs, **kwargs)
def _set_instance_name_for_function(self):
@@ -805,7 +937,7 @@ def getattr_fresh(self, attr):
The recomputed attribute value.
"""
if hasattr(self, attr):
- docassemble.base.functions.reconsider(self.instanceName + '.' + attr)
+ reconsider(self.instanceName + '.' + attr)
return getattr(self, attr)
def is_peer_relation(self, target, relationship_type, tree):
@@ -982,7 +1114,7 @@ def set_relationship(self, target, relationship_type, self_is, tree, replace=Fal
def get_point_of_view(self):
if hasattr(self, '_point_of_view'):
return self._point_of_view
- if self is docassemble.base.functions.this_thread.global_vars.user:
+ if self is this_thread.global_vars.user:
return '2'
return None
@@ -1078,14 +1210,14 @@ def _map_info(self):
def __getattr__(self, thename):
if thename.startswith('__') or hasattr(self.__class__, thename):
- if 'pending_error' in docassemble.base.functions.this_thread.misc:
- pending_error = docassemble.base.functions.this_thread.misc['pending_error']
- del docassemble.base.functions.this_thread.misc['pending_error']
+ if 'pending_error' in this_thread.misc:
+ pending_error = this_thread.misc['pending_error']
+ del this_thread.misc['pending_error']
raise pending_error
return object.__getattribute__(self, thename)
var_name = object.__getattribute__(self, 'instanceName') + "." + thename
- docassemble.base.functions.this_thread.misc['pending_error'] = DAAttributeError("name '" + var_name + "' is not defined")
- raise docassemble.base.functions.this_thread.misc['pending_error']
+ this_thread.misc['pending_error'] = DAAttributeError("name '" + var_name + "' is not defined")
+ raise this_thread.misc['pending_error']
def raise_undefined_attribute_error(self, thename):
"""Raise a DAAttributeError for the named attribute, as if the attribute were undefined.
@@ -1100,8 +1232,8 @@ def raise_undefined_attribute_error(self, thename):
DAAttributeError: Always raised.
"""
var_name = object.__getattribute__(self, 'instanceName') + "." + thename
- docassemble.base.functions.this_thread.misc['pending_error'] = DAAttributeError("name '" + var_name + "' is not defined")
- raise docassemble.base.functions.this_thread.misc['pending_error']
+ this_thread.misc['pending_error'] = DAAttributeError("name '" + var_name + "' is not defined")
+ raise this_thread.misc['pending_error']
def object_name(self, **kwargs):
"""Return a human-readable name for the object based on its instance name.
@@ -1129,7 +1261,7 @@ def as_serializable(self):
Returns:
dict: A serializable dict representation of the object.
"""
- return docassemble.base.functions.safe_json(self)
+ return safe_json(self)
def possessive(self, target, **kwargs):
"""Return a possessive phrase appropriate to this object.
@@ -1148,11 +1280,11 @@ def possessive(self, target, **kwargs):
if person == '2':
return your(target, **kwargs)
if person == '2p':
- return docassemble.base.functions.your_plural(target, **kwargs)
+ return your_plural(target, **kwargs)
if person == '1':
- return docassemble.base.functions.my_possessive(target, **kwargs)
+ return my_possessive(target, **kwargs)
if person == '1p':
- return docassemble.base.functions.our_possessive(target, **kwargs)
+ return our_possessive(target, **kwargs)
return possessify(self, target, **kwargs)
def object_possessive(self, target, **kwargs):
@@ -1183,13 +1315,13 @@ def is_are_you(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- output = docassemble.base.functions.are_you(**kwargs)
+ output = are_you(**kwargs)
if person == '2p':
- output = docassemble.base.functions.are_you_plural(**kwargs)
+ output = are_you_plural(**kwargs)
elif person == '1':
- output = docassemble.base.functions.am_i(**kwargs)
+ output = am_i(**kwargs)
elif person == '1p':
- output = docassemble.base.functions.are_we(**kwargs)
+ output = are_we(**kwargs)
else:
output = is_word(str(self), **kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
@@ -1208,13 +1340,13 @@ def yourself_or_name(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- output = docassemble.base.functions.yourself(**kwargs)
+ output = yourself(**kwargs)
elif person == '2p':
- output = docassemble.base.functions.yourselves(**kwargs)
+ output = yourselves(**kwargs)
elif person == '1':
- output = docassemble.base.functions.myself(**kwargs)
+ output = myself(**kwargs)
elif person == '1p':
- output = docassemble.base.functions.ourselves(**kwargs)
+ output = ourselves(**kwargs)
else:
output = str(self)
if 'capitalize' in kwargs and kwargs['capitalize']:
@@ -1233,20 +1365,20 @@ def itself(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- return docassemble.base.functions.yourself(**kwargs)
+ return yourself(**kwargs)
if person == '2p':
- return docassemble.base.functions.yourselves(**kwargs)
+ return yourselves(**kwargs)
if person == '1':
- return docassemble.base.functions.myself(**kwargs)
+ return myself(**kwargs)
if person == '1p':
- return docassemble.base.functions.ourselves(**kwargs)
- return docassemble.base.functions.itself(**kwargs)
+ return ourselves(**kwargs)
+ return itself(**kwargs)
def is_user(self):
"""Return True if this object is the current user, otherwise False."""
- return self is docassemble.base.functions.this_thread.global_vars.user
+ return self is this_thread.global_vars.user
- def initializeAttribute(self, *pargs, **kwargs):
+ def initialize_attribute(self, *pargs, **kwargs):
"""Define an attribute as a newly initialized DAObject, if not already defined.
The attribute will be created with an ``instanceName`` derived from
@@ -1264,7 +1396,7 @@ def initializeAttribute(self, *pargs, **kwargs):
"""
pargs = list(pargs)
if len(pargs) < 2:
- raise DAError("initializeAttribute requires an attribute name and an object type")
+ raise DAError("initialize_attribute requires an attribute name and an object type")
name = pargs.pop(0)
object_type = pargs.pop(0)
new_object_parameters = {}
@@ -1280,10 +1412,12 @@ def initializeAttribute(self, *pargs, **kwargs):
self.attrList.append(name)
return getattr(self, name)
- def reInitializeAttribute(self, *pargs, **kwargs):
+ initializeAttribute = initialize_attribute
+
+ def reinitialize_attribute(self, *pargs, **kwargs):
"""Redefine an attribute as a newly initialized DAObject, overwriting any existing value.
- Like ``initializeAttribute()``, but overwrites the attribute even if
+ Like ``initialize_attribute()``, but overwrites the attribute even if
it is already defined.
Args:
@@ -1311,6 +1445,8 @@ def reInitializeAttribute(self, *pargs, **kwargs):
self.attrList.append(name)
return getattr(self, name)
+ reInitializeAttribute = reinitialize_attribute
+
def attribute_defined(self, name):
"""Return True if the named attribute is defined, otherwise False.
@@ -1363,11 +1499,11 @@ def pronoun_possessive(self, target, **kwargs):
if person == '2':
output = your(target, **kwargs)
elif person == '2p':
- output = docassemble.base.functions.your_plural(target, **kwargs)
+ output = your_plural(target, **kwargs)
elif person == '1':
- output = docassemble.base.functions.my_possessive(target, **kwargs)
+ output = my_possessive(target, **kwargs)
elif person == '1p':
- output = docassemble.base.functions.our_possessive(target, **kwargs)
+ output = our_possessive(target, **kwargs)
else:
output = its(target, **kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
@@ -1388,15 +1524,15 @@ def pronoun(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- output = docassemble.base.functions.you_objective(**kwargs)
+ output = you_objective(**kwargs)
elif person == '2p':
- output = docassemble.base.functions.you_objective_plural(**kwargs)
+ output = you_objective_plural(**kwargs)
elif person == '1':
- output = docassemble.base.functions.me_objective(**kwargs)
+ output = me_objective(**kwargs)
elif person == '1p':
- output = docassemble.base.functions.us_objective(**kwargs)
+ output = us_objective(**kwargs)
else:
- output = docassemble.base.functions.it_objective(**kwargs)
+ output = it_objective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize_func(output)
return output
@@ -1426,15 +1562,15 @@ def pronoun_subjective(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- output = docassemble.base.functions.you_subjective(**kwargs)
+ output = you_subjective(**kwargs)
elif person == '2p':
- output = docassemble.base.functions.you_subjective_plural(**kwargs)
+ output = you_subjective_plural(**kwargs)
elif person == '1':
- output = docassemble.base.functions.i_subjective(**kwargs)
+ output = i_subjective(**kwargs)
elif person == '1p':
- output = docassemble.base.functions.we_subjective(**kwargs)
+ output = we_subjective(**kwargs)
else:
- output = docassemble.base.functions.it_subjective(**kwargs)
+ output = it_subjective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize_func(output)
return output
@@ -1478,11 +1614,11 @@ def do_question(self, the_verb, **kwargs):
if person == '2':
return do_you(the_verb, **kwargs)
if person == '2p':
- return docassemble.base.functions.do_you_plural(the_verb, **kwargs)
+ return do_you_plural(the_verb, **kwargs)
if person == '1':
- return docassemble.base.functions.do_i(the_verb, **kwargs)
+ return do_i(the_verb, **kwargs)
if person == '1p':
- return docassemble.base.functions.do_we(the_verb, **kwargs)
+ return do_we(the_verb, **kwargs)
return does_a_b(self, the_verb, **kwargs)
def did_question(self, the_verb, **kwargs):
@@ -1499,11 +1635,11 @@ def did_question(self, the_verb, **kwargs):
if person == '2':
return did_you(the_verb, **kwargs)
if person == '2p':
- return docassemble.base.functions.did_you_plural(the_verb, **kwargs)
+ return did_you_plural(the_verb, **kwargs)
if person == '1':
- return docassemble.base.functions.did_i(the_verb, **kwargs)
+ return did_i(the_verb, **kwargs)
if person == '1p':
- return docassemble.base.functions.did_we(the_verb, **kwargs)
+ return did_we(the_verb, **kwargs)
return did_a_b(self, the_verb, **kwargs)
def were_question(self, the_target, **kwargs):
@@ -1520,11 +1656,11 @@ def were_question(self, the_target, **kwargs):
if person == '2':
return were_you(the_target, **kwargs)
if person == '2p':
- return docassemble.base.functions.were_you_plural(the_target, **kwargs)
+ return were_you_plural(the_target, **kwargs)
if person == '1':
- return docassemble.base.functions.was_i(the_target, **kwargs)
+ return was_i(the_target, **kwargs)
if person == '1p':
- return docassemble.base.functions.were_we(the_target, **kwargs)
+ return were_we(the_target, **kwargs)
return was_a_b(self, the_target, **kwargs)
def have_question(self, the_target, **kwargs):
@@ -1541,11 +1677,11 @@ def have_question(self, the_target, **kwargs):
if person == '2':
return have_you(the_target, **kwargs)
if person == '2p':
- return docassemble.base.functions.have_you_plural(the_target, **kwargs)
+ return have_you_plural(the_target, **kwargs)
if person == '1':
- return docassemble.base.functions.have_i(the_target, **kwargs)
+ return have_i(the_target, **kwargs)
if person == '1p':
- return docassemble.base.functions.have_we(the_target, **kwargs)
+ return have_we(the_target, **kwargs)
return has_a_b(self, the_target, **kwargs)
def does_verb(self, the_verb, **kwargs):
@@ -1609,13 +1745,13 @@ def subjective_pronoun_or_name(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- output = docassemble.base.functions.you_subjective(**kwargs)
+ output = you_subjective(**kwargs)
elif person == '2p':
- output = docassemble.base.functions.you_subjective_plural(**kwargs)
+ output = you_subjective_plural(**kwargs)
elif person == '1':
- output = docassemble.base.functions.i_subjective(**kwargs)
+ output = i_subjective(**kwargs)
elif person == '1p':
- output = docassemble.base.functions.we_subjective(**kwargs)
+ output = we_subjective(**kwargs)
else:
output = str(self)
if 'capitalize' in kwargs and kwargs['capitalize']:
@@ -1634,13 +1770,13 @@ def pronoun_or_name(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- output = docassemble.base.functions.you_objective(**kwargs)
+ output = you_objective(**kwargs)
elif person == '2p':
- output = docassemble.base.functions.you_objective_plural(**kwargs)
+ output = you_objective_plural(**kwargs)
elif person == '1':
- output = docassemble.base.functions.me_objective(**kwargs)
+ output = me_objective(**kwargs)
elif person == '1p':
- output = docassemble.base.functions.us_objective(**kwargs)
+ output = us_objective(**kwargs)
else:
output = str(self)
if 'capitalize' in kwargs and kwargs['capitalize']:
@@ -2272,7 +2408,7 @@ def init(self, *pargs, **kwargs):
self.ask_object_type = False
super().init(*pargs, **kwargs)
- def initializeObject(self, *pargs, **kwargs):
+ def initializeObject(self, *pargs, **kwargs): # pylint: disable=invalid-name
"""Create a new object at a given index in the list.
Args:
@@ -2290,44 +2426,46 @@ def initializeObject(self, *pargs, **kwargs):
Raises:
DAError: If the first argument is not a non-negative integer.
"""
- objectFunction = None
+ object_function = None
pargs = list(pargs)
if len(pargs) == 0 or not isinstance(pargs[0], int) or pargs[0] < 0:
raise DAError("initializeObject: first parameter must be an integer (0 or greater)")
index = pargs.pop(0)
if len(pargs) > 0:
- objectFunction = pargs.pop(0)
+ object_function = pargs.pop(0)
new_obj_parameters = {}
- if isinstance(objectFunction, DAObjectPlusParameters):
- for key, val in objectFunction.parameters.items():
+ if isinstance(object_function, DAObjectPlusParameters):
+ for key, val in object_function.parameters.items():
new_obj_parameters[key] = val
- objectFunction = objectFunction.object_type
- if objectFunction is None:
+ object_function = object_function.object_type
+ if object_function is None:
if self.ask_object_type:
if isinstance(self.new_object_type, DAObjectPlusParameters):
- objectFunction = self.new_object_type.object_type
+ object_function = self.new_object_type.object_type
new_obj_parameters = self.new_object_type.parameters
elif isinstance(self.new_object_type, type):
- objectFunction = self.new_object_type
+ object_function = self.new_object_type
else:
raise DAError("new_object_type must be an object type")
elif self.object_type is not None:
- objectFunction = self.object_type
+ object_function = self.object_type
for key, val in self.object_type_parameters.items():
new_obj_parameters[key] = val
else:
- objectFunction = DAObject
+ object_function = DAObject
for key, val in kwargs.items():
new_obj_parameters[key] = val
- newobject = objectFunction(self.instanceName + '[' + repr(index) + ']', *pargs, **new_obj_parameters)
+ newobject = object_function(self.instanceName + '[' + repr(index) + ']', *pargs, **new_obj_parameters)
for pre_index in range(len(self.elements), index): # pylint: disable=unused-variable
self.elements.append(None)
self[index] = newobject
self.there_are_any = True
- if objectFunction is None and self.ask_object_type and hasattr(self, 'new_object_type'):
+ if object_function is None and self.ask_object_type and hasattr(self, 'new_object_type'):
delattr(self, 'new_object_type')
return newobject
+ initialize_object = initializeObject
+
def set_object_type(self, object_type):
"""Set the object type used when creating new list items.
@@ -2343,10 +2481,10 @@ def set_object_type(self, object_type):
self.object_type_parameters = {}
def cancel_add_or_edit(self):
- unique_id = docassemble.base.functions.this_thread.current_info['user']['session_uid']
- if 'event_stack' in docassemble.base.functions.this_thread.internal and unique_id in docassemble.base.functions.this_thread.internal['event_stack']:
+ unique_id = this_thread.current_info['user']['session_uid']
+ if 'event_stack' in this_thread.internal and unique_id in this_thread.internal['event_stack']:
new_stack = []
- for item in docassemble.base.functions.this_thread.internal['event_stack'][unique_id]:
+ for item in this_thread.internal['event_stack'][unique_id]:
if 'arguments' in item:
if 'list' in item['arguments'] and item['arguments']['list'] == self.instanceName:
continue
@@ -2355,7 +2493,7 @@ def cancel_add_or_edit(self):
if 'action' in item and item['action'].startswith(self.instanceName + '['):
continue
new_stack.append(item)
- docassemble.base.functions.this_thread.internal['event_stack'][unique_id] = new_stack
+ this_thread.internal['event_stack'][unique_id] = new_stack
if self.complete_elements().number() < self.number_gathered():
self.pop()
self.delattr('doing_gathered_and_complete', '_necessary_length', 'there_is_one_other')
@@ -2452,7 +2590,7 @@ def filter(self, *pargs, **kwargs):
def _trigger_gather(self):
"""Triggers the gathering process."""
- if docassemble.base.functions.get_gathering_mode(self.instanceName) is False:
+ if get_gathering_mode(self.instanceName) is False:
if self.auto_gather:
self.gather()
else:
@@ -2655,7 +2793,7 @@ def sort_elements(self, *pargs, **kwargs): # pylint: disable=unused-argument
self._reset_instance_names()
return self
- def appendObject(self, *pargs, **kwargs):
+ def appendObject(self, *pargs, **kwargs): # pylint: disable=invalid-name
"""Create a new object and append it to the list.
Args:
@@ -2670,35 +2808,37 @@ def appendObject(self, *pargs, **kwargs):
DAObject: The newly created object appended to the list.
"""
# logmessage("Called appendObject where len is " + str(len(self.elements)))
- objectFunction = None
+ object_function = None
if len(pargs) > 0:
pargs = list(pargs)
- objectFunction = pargs.pop(0)
+ object_function = pargs.pop(0)
new_obj_parameters = {}
- if objectFunction is None:
+ if object_function is None:
if self.ask_object_type:
if isinstance(self.new_object_type, DAObjectPlusParameters):
- objectFunction = self.new_object_type.object_type
+ object_function = self.new_object_type.object_type
new_obj_parameters = self.new_object_type.parameters
elif isinstance(self.new_object_type, type):
- objectFunction = self.new_object_type
+ object_function = self.new_object_type
else:
raise DAError("new_object_type must be an object type")
elif self.object_type is not None:
- objectFunction = self.object_type
+ object_function = self.object_type
for key, val in self.object_type_parameters.items():
new_obj_parameters[key] = val
else:
- objectFunction = DAObject
+ object_function = DAObject
for key, val in kwargs.items():
new_obj_parameters[key] = val
- newobject = objectFunction(self.instanceName + '[' + str(len(self.elements)) + ']', *pargs, **new_obj_parameters)
+ newobject = object_function(self.instanceName + '[' + str(len(self.elements)) + ']', *pargs, **new_obj_parameters)
self.elements.append(newobject)
self.there_are_any = True
- if objectFunction is None and self.ask_object_type and hasattr(self, 'new_object_type'):
+ if object_function is None and self.ask_object_type and hasattr(self, 'new_object_type'):
delattr(self, 'new_object_type')
return newobject
+ append_object = appendObject
+
def append(self, *pargs, **kwargs):
"""Add one or more items to the end of the list.
@@ -2792,13 +2932,13 @@ def itself(self, **kwargs):
if self.number() == 1:
if isinstance(self.elements[0], DAObject):
return self.elements[0].itself(**kwargs)
- return docassemble.base.functions.itself(**kwargs)
+ return itself(**kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.yourselves(**kwargs)
+ return yourselves(**kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.ourselves(**kwargs)
- return docassemble.base.functions.themselves(**kwargs)
+ return ourselves(**kwargs)
+ return themselves(**kwargs)
def do_question(self, the_verb, **kwargs):
"""Given a verb like "eat," returns "do x eat" if there is
@@ -2814,10 +2954,10 @@ def do_question(self, the_verb, **kwargs):
return does_a_b(self.elements[0], the_verb, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.do_you_plural(the_verb, **kwargs)
+ return do_you_plural(the_verb, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.do_we(the_verb, **kwargs)
- return docassemble.base.functions.do_a_b(self, the_verb, **kwargs)
+ return do_we(the_verb, **kwargs)
+ return do_a_b(self, the_verb, **kwargs)
def did_question(self, the_verb, **kwargs):
"""Given a verb like "eat," returns "did x eat" if there is
@@ -2833,10 +2973,10 @@ def did_question(self, the_verb, **kwargs):
return did_a_b(self.elements[0], the_verb, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.did_you_plural(the_verb, **kwargs)
+ return did_you_plural(the_verb, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.did_we(the_verb, **kwargs)
- return docassemble.base.functions.did_a_b_plural(self, the_verb, **kwargs)
+ return did_we(the_verb, **kwargs)
+ return did_a_b_plural(self, the_verb, **kwargs)
def were_question(self, the_target, **kwargs):
"""Given a target like "married", returns "were x married" if
@@ -2850,10 +2990,10 @@ def were_question(self, the_target, **kwargs):
return was_a_b(self.elements[0], the_target, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.were_you_plural(the_target, **kwargs)
+ return were_you_plural(the_target, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.were_we(the_target, **kwargs)
- return docassemble.base.functions.were_a_b_plural(self, the_target, **kwargs)
+ return were_we(the_target, **kwargs)
+ return were_a_b_plural(self, the_target, **kwargs)
def have_question(self, the_target, **kwargs):
"""Given a target like "married", returns "have x married" if
@@ -2869,10 +3009,10 @@ def have_question(self, the_target, **kwargs):
return has_a_b(self.elements[0], the_target, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.have_you_plural(the_target, **kwargs)
+ return have_you_plural(the_target, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.have_we(the_target, **kwargs)
- return docassemble.base.functions.have_a_b(self, the_target, **kwargs)
+ return have_we(the_target, **kwargs)
+ return have_a_b(self, the_target, **kwargs)
def does_verb(self, the_verb, **kwargs):
"""Return the correctly conjugated present-tense form of a verb for the list.
@@ -2997,11 +3137,11 @@ def is_are_you(self, **kwargs):
return self.elements[0].is_are_you(**kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- output = docassemble.base.functions.are_you_plural(**kwargs)
+ output = are_you_plural(**kwargs)
elif person in ('1', '1p'):
- output = docassemble.base.functions.are_we(**kwargs)
+ output = are_we(**kwargs)
else:
- output = docassemble.base.functions.are_word(str(self), **kwargs)
+ output = are_word(str(self), **kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize(output)
return output
@@ -3231,7 +3371,7 @@ def gather(self, number=None, item_object_type=None, minimum=None, complete_attr
item_object_parameters = {}
if complete_attribute is None and self.complete_attribute is not None:
complete_attribute = self.complete_attribute
- docassemble.base.functions.set_gathering_mode(True, self.instanceName)
+ set_gathering_mode(True, self.instanceName)
if number is None and self.ask_number:
if hasattr(self, 'there_are_any') and not self.there_are_any:
number = 0
@@ -3303,7 +3443,7 @@ def gather(self, number=None, item_object_type=None, minimum=None, complete_attr
# del self.doing_gathered_and_complete
if hasattr(self, 'was_gathered'):
del self.was_gathered
- docassemble.base.functions.set_gathering_mode(False, self.instanceName)
+ set_gathering_mode(False, self.instanceName)
self.hook_after_gather()
return True
@@ -3380,7 +3520,7 @@ def __getitem__(self, index):
try:
return self.elements[index]
except:
- if (self.auto_gather and hasattr(self, 'gathered') and not (hasattr(self, '_appending_allowed') and self._appending_allowed)) or docassemble.base.functions.this_thread.probing:
+ if (self.auto_gather and hasattr(self, 'gathered') and not (hasattr(self, '_appending_allowed') and self._appending_allowed)) or this_thread.probing:
try:
logmessage("list index " + str(index) + " out of range on " + str(self.instanceName))
except:
@@ -3502,9 +3642,9 @@ def pronoun_possessive(self, target, **kwargs):
return its(target, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- output = docassemble.base.functions.your_plural(target, **kwargs)
+ output = your_plural(target, **kwargs)
elif person in ('1', '1p'):
- output = docassemble.base.functions.our_possessive(target, **kwargs)
+ output = our_possessive(target, **kwargs)
else:
output = their(target, **kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
@@ -3527,14 +3667,14 @@ def pronoun(self, **kwargs):
self._trigger_gather()
if isinstance(self.elements[0], DAObject):
return self.elements[0].pronoun(**kwargs)
- return docassemble.base.functions.it_objective(**kwargs)
+ return it_objective(**kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- output = docassemble.base.functions.you_objective_plural(**kwargs)
+ output = you_objective_plural(**kwargs)
elif person in ('1', '1p'):
- output = docassemble.base.functions.us_objective(**kwargs)
+ output = us_objective(**kwargs)
else:
- output = docassemble.base.functions.them_objective(**kwargs)
+ output = them_objective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize_func(output)
return output
@@ -3559,14 +3699,14 @@ def pronoun_subjective(self, **kwargs):
self._trigger_gather()
if isinstance(self.elements[0], DAObject):
return self.elements[0].pronoun_subjective(**kwargs)
- docassemble.base.functions.it_subjective(**kwargs)
+ it_subjective(**kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- output = docassemble.base.functions.you_subjective_plural(**kwargs)
+ output = you_subjective_plural(**kwargs)
elif person in ('1', '1p'):
- output = docassemble.base.functions.we_subjective(**kwargs)
+ output = we_subjective(**kwargs)
else:
- output = docassemble.base.functions.they_subjective(**kwargs)
+ output = they_subjective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize_func(output)
return output
@@ -3583,7 +3723,7 @@ def _reorder(self, *pargs):
self.hook_after_gather()
def _reorder_buttons(self, classes, index):
- return '' + word("Move up") + ' ' + word("Move down") + ' '
+ return '' + word("Move up") + ' ' + word("Move down") + ' '
def _edit_button(self, url, classes):
return f' {word("Edit")} '
@@ -3611,7 +3751,7 @@ def item_actions(self, *pargs, **kwargs):
index = the_args.pop(0)
output = ''
if kwargs.get('reorder', False):
- output += self._reorder_buttons('btn btn-sm ' + server.button_class_prefix + server.daconfig['button colors'].get('reorder', 'info') + ' btn-darevisit datableup', index)
+ output += self._reorder_buttons('btn btn-sm ' + get_button_class_prefix() + get_configuration()['button colors'].get('reorder', 'info') + ' btn-darevisit datableup', index)
if self.minimum_number is not None and len(self.elements) <= self.minimum_number:
can_delete = False
else:
@@ -3640,17 +3780,17 @@ def item_actions(self, *pargs, **kwargs):
items += [{'action': '_da_define', 'arguments': {'variables': [item.instanceName + '.' + attrib for attrib in self._complete_attributes()]}}]
if ensure_complete:
items += [{'action': '_da_list_ensure_complete', 'arguments': {'group': self.instanceName}}]
- output += self._edit_button(docassemble.base.functions.url_action('_da_list_edit', items=items), 'btn btn-sm ' + server.button_class_prefix + server.daconfig['button colors'].get('edit', 'secondary') + ' btn-darevisit')
+ output += self._edit_button(url_action('_da_list_edit', items=items), 'btn btn-sm ' + get_button_class_prefix() + get_configuration()['button colors'].get('edit', 'secondary') + ' btn-darevisit')
if use_delete and can_delete:
if kwargs.get('confirm', False):
areyousure = ' daremovebutton'
else:
areyousure = ''
- output += self._delete_button(docassemble.base.functions.url_action('_da_list_remove', list=self.instanceName, item=repr(index)), 'btn btn-sm ' + server.button_class_prefix + server.daconfig['button colors'].get('delete', 'danger') + ' btn-darevisit' + areyousure)
+ output += self._delete_button(url_action('_da_list_remove', list=self.instanceName, item=repr(index)), 'btn btn-sm ' + get_button_class_prefix() + get_configuration()['button colors'].get('delete', 'danger') + ' btn-darevisit' + areyousure)
if kwargs.get('edit_url_only', False):
- return docassemble.base.functions.url_action('_da_list_edit', items=items)
+ return url_action('_da_list_edit', items=items)
if kwargs.get('delete_url_only', False):
- return docassemble.base.functions.url_action('_da_list_remove', dict=self.instanceName, item=repr(index))
+ return url_action('_da_list_remove', dict=self.instanceName, item=repr(index))
return output
def _add_action_button(self, url, classes, icon, the_message):
@@ -3680,7 +3820,7 @@ def add_action(self, label=None, message=None, url_only=False, icon='plus-circle
str: HTML anchor element or URL string.
"""
if color is None:
- color = server.daconfig['button colors'].get('add', 'secondary')
+ color = get_configuration()['button colors'].get('add', 'secondary')
if color not in ('primary', 'secondary', 'tertiary', 'success', 'danger', 'warning', 'info', 'light', 'dark'):
color = 'success'
if size not in ('sm', 'md', 'lg'):
@@ -3717,10 +3857,10 @@ def add_action(self, label=None, message=None, url_only=False, icon='plus-circle
message = word("Add an item")
else:
message = word(str(message))
- the_url = docassemble.base.functions.url_action('_da_list_add', list=self.instanceName)
+ the_url = url_action('_da_list_add', list=self.instanceName)
if url_only:
return the_url
- return self._add_action_button(the_url, 'btn' + size + block + ' ' + server.button_class_prefix + color + ' btn-darevisit' + classname, icon, message)
+ return self._add_action_button(the_url, 'btn' + size + block + ' ' + get_button_class_prefix() + color + ' btn-darevisit' + classname, icon, message)
def hook_on_gather(self, *pargs, **kwargs):
"""Override this method to run code just before the list is marked as gathered."""
@@ -3830,7 +3970,7 @@ def set_object_type(self, object_type):
def _trigger_gather(self):
"""Triggers the gathering process."""
- if docassemble.base.functions.get_gathering_mode(self.instanceName) is False:
+ if get_gathering_mode(self.instanceName) is False:
if self.auto_gather:
self.gather()
else:
@@ -3906,7 +4046,7 @@ def getitem_fresh(self, item):
object: The freshly computed value for ``item``.
"""
if item in self.elements:
- docassemble.base.functions.reconsider(self.item_name(item))
+ reconsider(self.item_name(item))
return self[item]
def all_false(self, *pargs, **kwargs):
@@ -4051,7 +4191,7 @@ def _sorted_iteritems(self):
def _sorted_elements_iteritems(self):
return sorted(self.elements.items())
- def initializeObject(self, *pargs, **kwargs):
+ def initializeObject(self, *pargs, **kwargs): # pylint: disable=invalid-name
"""Create a new object and store it at the given key in the dictionary.
Args:
@@ -4066,40 +4206,42 @@ def initializeObject(self, *pargs, **kwargs):
Returns:
DAObject: The newly created object stored at ``self[entry]``.
"""
- objectFunction = None
+ object_function = None
pargs = list(pargs)
entry = pargs.pop(0)
if len(pargs) > 0:
- objectFunction = pargs.pop(0)
+ object_function = pargs.pop(0)
new_obj_parameters = {}
- if isinstance(objectFunction, DAObjectPlusParameters):
- for key, val in objectFunction.parameters.items():
+ if isinstance(object_function, DAObjectPlusParameters):
+ for key, val in object_function.parameters.items():
new_obj_parameters[key] = val
- objectFunction = objectFunction.object_type
- if objectFunction is None:
+ object_function = object_function.object_type
+ if object_function is None:
if self.ask_object_type:
if isinstance(self.new_object_type, DAObjectPlusParameters):
- objectFunction = self.new_object_type.object_type
+ object_function = self.new_object_type.object_type
new_obj_parameters = self.new_object_type.parameters
elif isinstance(self.new_object_type, type):
- objectFunction = self.new_object_type
+ object_function = self.new_object_type
else:
raise DAError("new_object_type must be an object type")
elif self.object_type is not None:
- objectFunction = self.object_type
+ object_function = self.object_type
for key, val in self.object_type_parameters.items():
new_obj_parameters[key] = val
else:
- objectFunction = DAObject
+ object_function = DAObject
for key, val in kwargs.items():
new_obj_parameters[key] = val
- newobject = objectFunction(self.instanceName + '[' + repr(entry) + ']', *pargs, **new_obj_parameters)
+ newobject = object_function(self.instanceName + '[' + repr(entry) + ']', *pargs, **new_obj_parameters)
self[entry] = newobject
self.there_are_any = True
- if objectFunction is None and self.ask_object_type and hasattr(self, 'new_object_type'):
+ if object_function is None and self.ask_object_type and hasattr(self, 'new_object_type'):
delattr(self, 'new_object_type')
return newobject
+ initialize_object = initializeObject
+
def new(self, *pargs, **kwargs):
"""Initialize new dictionary entries as DAObject instances.
@@ -4214,13 +4356,13 @@ def itself(self, **kwargs):
first_element = list(self.elements.values())[0]
if isinstance(first_element, DAObject):
return first_element.itself(**kwargs)
- return docassemble.base.functions.itself(**kwargs)
+ return itself(**kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.yourselves(**kwargs)
+ return yourselves(**kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.ourselves(**kwargs)
- return docassemble.base.functions.themselves(**kwargs)
+ return ourselves(**kwargs)
+ return themselves(**kwargs)
def do_question(self, the_verb, **kwargs):
"""Given a verb like "eat," returns "do x eat" if there is
@@ -4237,10 +4379,10 @@ def do_question(self, the_verb, **kwargs):
return does_a_b(first_element, the_verb, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.do_you_plural(the_verb, **kwargs)
+ return do_you_plural(the_verb, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.do_we(the_verb, **kwargs)
- return docassemble.base.functions.do_a_b(self, the_verb, **kwargs)
+ return do_we(the_verb, **kwargs)
+ return do_a_b(self, the_verb, **kwargs)
def did_question(self, the_verb, **kwargs):
"""Given a verb like "eat," returns "did x eat" if there is
@@ -4257,10 +4399,10 @@ def did_question(self, the_verb, **kwargs):
return did_a_b(first_element, the_verb, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.did_you_plural(the_verb, **kwargs)
+ return did_you_plural(the_verb, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.did_we(the_verb, **kwargs)
- return docassemble.base.functions.did_a_b_plural(self, the_verb, **kwargs)
+ return did_we(the_verb, **kwargs)
+ return did_a_b_plural(self, the_verb, **kwargs)
def were_question(self, the_target, **kwargs):
"""Given a target like "married", returns "were x married" if
@@ -4277,10 +4419,10 @@ def were_question(self, the_target, **kwargs):
return was_a_b(first_element, the_target, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.were_you_plural(the_target, **kwargs)
+ return were_you_plural(the_target, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.were_we(the_target, **kwargs)
- return docassemble.base.functions.were_a_b_plural(self, the_target, **kwargs)
+ return were_we(the_target, **kwargs)
+ return were_a_b_plural(self, the_target, **kwargs)
def have_question(self, the_target, **kwargs):
"""Given a target like "married", returns "have x married" if
@@ -4297,10 +4439,10 @@ def have_question(self, the_target, **kwargs):
return has_a_b(first_element, the_target, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.have_you_plural(the_target, **kwargs)
+ return have_you_plural(the_target, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.have_we(the_target, **kwargs)
- return docassemble.base.functions.have_a_b(self, the_target, **kwargs)
+ return have_we(the_target, **kwargs)
+ return have_a_b(self, the_target, **kwargs)
def does_verb(self, the_verb, **kwargs):
"""Return the correctly conjugated present-tense form of a verb for the dictionary.
@@ -4575,10 +4717,10 @@ def _validate(self, item_object_type, complete_attribute, keys=None):
str(elem)
def cancel_add_or_edit(self):
- unique_id = docassemble.base.functions.this_thread.current_info['user']['session_uid']
- if 'event_stack' in docassemble.base.functions.this_thread.internal and unique_id in docassemble.base.functions.this_thread.internal['event_stack']:
+ unique_id = this_thread.current_info['user']['session_uid']
+ if 'event_stack' in this_thread.internal and unique_id in this_thread.internal['event_stack']:
new_stack = []
- for item in docassemble.base.functions.this_thread.internal['event_stack'][unique_id]:
+ for item in this_thread.internal['event_stack'][unique_id]:
if 'arguments' in item:
if 'dict' in item['arguments'] and item['arguments']['dict'] == self.instanceName:
continue
@@ -4587,7 +4729,7 @@ def cancel_add_or_edit(self):
if 'action' in item and item['action'].startswith(self.instanceName + '['):
continue
new_stack.append(item)
- docassemble.base.functions.this_thread.internal['event_stack'][unique_id] = new_stack
+ this_thread.internal['event_stack'][unique_id] = new_stack
if self.complete_elements().number() < self.number_gathered():
self.popitem()
self.delattr('doing_gathered_and_complete', 'there_is_one_other', 'new_item_name')
@@ -4651,7 +4793,7 @@ def gather(self, item_object_type=None, number=None, minimum=None, complete_attr
new_item_parameters = {}
if complete_attribute is None and self.complete_attribute is not None:
complete_attribute = self.complete_attribute
- docassemble.base.functions.set_gathering_mode(True, self.instanceName)
+ set_gathering_mode(True, self.instanceName)
self._validate(item_object_type, complete_attribute, keys=keys)
if number is None and self.ask_number:
if hasattr(self, 'there_are_any') and not self.there_are_any:
@@ -4719,7 +4861,7 @@ def gather(self, item_object_type=None, number=None, minimum=None, complete_attr
if self.auto_gather:
self.gathered = True
self.revisit = True
- docassemble.base.functions.set_gathering_mode(False, self.instanceName)
+ set_gathering_mode(False, self.instanceName)
self.hook_after_gather()
return True
@@ -4755,7 +4897,7 @@ def comma_and_list(self, **kwargs):
def __getitem__(self, index):
if index not in self.elements:
- if (self.object_type is None and not self.ask_object_type) or docassemble.base.functions.this_thread.probing:
+ if (self.object_type is None and not self.ask_object_type) or this_thread.probing:
var_name = object.__getattribute__(self, 'instanceName') + "[" + repr(index) + "]"
raise DAIndexError("name '" + var_name + "' is not defined")
if self.ask_object_type:
@@ -5043,11 +5185,11 @@ def pronoun_possessive(self, target, **kwargs):
if person == '2':
output = your(target, **kwargs)
elif person == '2p':
- output = docassemble.base.functions.your_plural(target, **kwargs)
+ output = your_plural(target, **kwargs)
elif person == '1':
- output = docassemble.base.functions.my_possessive(target, **kwargs)
+ output = my_possessive(target, **kwargs)
elif person == '1p':
- output = docassemble.base.functions.our_possessive(**kwargs)
+ output = our_possessive(**kwargs)
else:
output = their(target, **kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
@@ -5070,14 +5212,14 @@ def pronoun(self, **kwargs):
self._trigger_gather()
if isinstance(list(self.elements.values())[0], DAObject):
return list(self.elements.values())[0].pronoun(**kwargs)
- return docassemble.base.functions.it_objective(**kwargs)
+ return it_objective(**kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- output = docassemble.base.functions.you_objective_plural(**kwargs)
+ output = you_objective_plural(**kwargs)
elif person in ('1', '1p'):
- output = docassemble.base.functions.us_objective(**kwargs)
+ output = us_objective(**kwargs)
else:
- output = docassemble.base.functions.them_objective(**kwargs)
+ output = them_objective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize_func(output)
return output
@@ -5102,14 +5244,14 @@ def pronoun_subjective(self, **kwargs):
self._trigger_gather()
if isinstance(list(self.elements.values())[0], DAObject):
return list(self.elements.values())[0].pronoun_subjective(**kwargs)
- docassemble.base.functions.it_subjective(**kwargs)
+ it_subjective(**kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- output = docassemble.base.functions.you_subjective_plural(**kwargs)
+ output = you_subjective_plural(**kwargs)
elif person in ('1', '1p'):
- output = docassemble.base.functions.we_subjective(**kwargs)
+ output = we_subjective(**kwargs)
else:
- output = docassemble.base.functions.they_subjective(**kwargs)
+ output = they_subjective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize_func(output)
return output
@@ -5167,17 +5309,17 @@ def item_actions(self, *pargs, **kwargs):
items += [{'action': '_da_define', 'arguments': {'variables': [item.instanceName + '.' + attrib for attrib in self._complete_attributes()]}}]
if ensure_complete:
items += [{'action': '_da_dict_ensure_complete', 'arguments': {'group': self.instanceName}}]
- output += self._edit_button(docassemble.base.functions.url_action('_da_dict_edit', items=items), 'btn btn-sm ' + server.button_class_prefix + server.daconfig['button colors'].get('edit', 'secondary') + ' btn-darevisit')
+ output += self._edit_button(url_action('_da_dict_edit', items=items), 'btn btn-sm ' + get_button_class_prefix() + get_configuration()['button colors'].get('edit', 'secondary') + ' btn-darevisit')
if use_delete and can_delete:
if kwargs.get('confirm', False):
areyousure = ' daremovebutton'
else:
areyousure = ''
- output += self._delete_button(docassemble.base.functions.url_action('_da_dict_remove', dict=self.instanceName, item=repr(index)), 'btn btn-sm ' + server.button_class_prefix + server.daconfig['button colors'].get('delete', 'danger') + ' btn-darevisit' + areyousure)
+ output += self._delete_button(url_action('_da_dict_remove', dict=self.instanceName, item=repr(index)), 'btn btn-sm ' + get_button_class_prefix() + get_configuration()['button colors'].get('delete', 'danger') + ' btn-darevisit' + areyousure)
if kwargs.get('edit_url_only', False):
- return docassemble.base.functions.url_action('_da_dict_edit', items=items)
+ return url_action('_da_dict_edit', items=items)
if kwargs.get('delete_url_only', False):
- return docassemble.base.functions.url_action('_da_dict_remove', dict=self.instanceName, item=repr(index))
+ return url_action('_da_dict_remove', dict=self.instanceName, item=repr(index))
return output
def _add_action_button(self, url, classes, icon, the_message):
@@ -5188,7 +5330,7 @@ def _add_action_button(self, url, classes, icon, the_message):
def add_action(self, label=None, message=None, url_only=False, icon='plus-circle', color=None, size='sm', block=None, classname=None): # pylint: disable=redefined-outer-name
"""Returns HTML for adding an item to a dict"""
if color is None:
- color = server.daconfig['button colors'].get('add', 'secondary')
+ color = get_configuration()['button colors'].get('add', 'secondary')
if color not in ('primary', 'secondary', 'tertiary', 'success', 'danger', 'warning', 'info', 'light', 'dark'):
color = 'success'
if size not in ('sm', 'md', 'lg'):
@@ -5225,10 +5367,10 @@ def add_action(self, label=None, message=None, url_only=False, icon='plus-circle
message = word("Add an item")
else:
message = word(str(message))
- the_url = docassemble.base.functions.url_action('_da_dict_add', dict=self.instanceName)
+ the_url = url_action('_da_dict_add', dict=self.instanceName)
if url_only:
return the_url
- return self._add_action_button(the_url, 'btn' + size + block + ' ' + server.button_class_prefix + color + ' btn-darevisit' + classname, icon, message)
+ return self._add_action_button(the_url, 'btn' + size + block + ' ' + get_button_class_prefix() + color + ' btn-darevisit' + classname, icon, message)
def _new_elements(self):
return {}
@@ -5419,7 +5561,7 @@ def filter(self, *pargs, **kwargs):
def _trigger_gather(self):
"""Triggers the gathering process."""
- if docassemble.base.functions.get_gathering_mode(self.instanceName) is False:
+ if get_gathering_mode(self.instanceName) is False:
if self.auto_gather:
self.gather()
else:
@@ -5556,13 +5698,13 @@ def itself(self, **kwargs):
first_element = list(self.elements)[0]
if isinstance(first_element, DAObject):
return first_element.itself(**kwargs)
- return docassemble.base.functions.itself(**kwargs)
+ return itself(**kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.yourselves(**kwargs)
+ return yourselves(**kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.ourselves(**kwargs)
- return docassemble.base.functions.themselves(**kwargs)
+ return ourselves(**kwargs)
+ return themselves(**kwargs)
def do_question(self, the_verb, **kwargs):
"""Given a verb like "eat," returns "do x eat" if there is
@@ -5579,10 +5721,10 @@ def do_question(self, the_verb, **kwargs):
return does_a_b(first_element, the_verb, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.do_you_plural(the_verb, **kwargs)
+ return do_you_plural(the_verb, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.do_we(the_verb, **kwargs)
- return docassemble.base.functions.do_a_b(self, the_verb, **kwargs)
+ return do_we(the_verb, **kwargs)
+ return do_a_b(self, the_verb, **kwargs)
def did_question(self, the_verb, **kwargs):
"""Given a verb like "eat," returns "did x eat" if there is
@@ -5599,10 +5741,10 @@ def did_question(self, the_verb, **kwargs):
return did_a_b(first_element, the_verb, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.did_you_plural(the_verb, **kwargs)
+ return did_you_plural(the_verb, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.did_we(the_verb, **kwargs)
- return docassemble.base.functions.did_a_b_plural(self, the_verb, **kwargs)
+ return did_we(the_verb, **kwargs)
+ return did_a_b_plural(self, the_verb, **kwargs)
def were_question(self, the_target, **kwargs):
"""Given a target like "married", returns "were x married" if
@@ -5619,10 +5761,10 @@ def were_question(self, the_target, **kwargs):
return was_a_b(first_element, the_target, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.were_you_plural(the_target, **kwargs)
+ return were_you_plural(the_target, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.were_we(the_target, **kwargs)
- return docassemble.base.functions.were_a_b_plural(self, the_target, **kwargs)
+ return were_we(the_target, **kwargs)
+ return were_a_b_plural(self, the_target, **kwargs)
def have_question(self, the_target, **kwargs):
"""Given a target like "married", returns "have x married" if
@@ -5639,10 +5781,10 @@ def have_question(self, the_target, **kwargs):
return has_a_b(first_element, the_target, **kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- return docassemble.base.functions.have_you_plural(the_target, **kwargs)
+ return have_you_plural(the_target, **kwargs)
if person in ('1', '1p'):
- return docassemble.base.functions.have_we(the_target, **kwargs)
- return docassemble.base.functions.have_a_b(self, the_target, **kwargs)
+ return have_we(the_target, **kwargs)
+ return have_a_b(self, the_target, **kwargs)
def does_verb(self, the_verb, **kwargs):
"""Return the correctly conjugated present-tense form of a verb for the set.
@@ -5861,7 +6003,7 @@ def gather(self, number=None, minimum=None):
return True
if not self.auto_gather:
return self.gathered
- docassemble.base.functions.set_gathering_mode(True, self.instanceName)
+ set_gathering_mode(True, self.instanceName)
for elem in sorted(self.elements):
str(elem)
if number is None and self.ask_number:
@@ -5893,7 +6035,7 @@ def gather(self, number=None, minimum=None):
if self.auto_gather:
self.gathered = True
self.revisit = True
- docassemble.base.functions.set_gathering_mode(False, self.instanceName)
+ set_gathering_mode(False, self.instanceName)
self.hook_after_gather()
return True
@@ -6074,11 +6216,11 @@ def pronoun_possessive(self, target, **kwargs):
if person == '2':
output = your(target, **kwargs)
elif person == '2p':
- output = docassemble.base.functions.your_plural(target, **kwargs)
+ output = your_plural(target, **kwargs)
elif person == '1':
- output = docassemble.base.functions.my_possessive(target, **kwargs)
+ output = my_possessive(target, **kwargs)
elif person == '1p':
- output = docassemble.base.functions.our_possessive(target, **kwargs)
+ output = our_possessive(target, **kwargs)
else:
output = their(target, **kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
@@ -6101,14 +6243,14 @@ def pronoun(self, **kwargs):
self._trigger_gather()
if isinstance(list(self.elements)[0], DAObject):
return list(self.elements)[0].pronoun(**kwargs)
- return docassemble.base.functions.it_objective(**kwargs)
+ return it_objective(**kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- output = docassemble.base.functions.you_objective_plural(**kwargs)
+ output = you_objective_plural(**kwargs)
elif person in ('1', '1p'):
- output = docassemble.base.functions.us_objective(**kwargs)
+ output = us_objective(**kwargs)
else:
- output = docassemble.base.functions.them_objective(**kwargs)
+ output = them_objective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize_func(output)
return output
@@ -6133,14 +6275,14 @@ def pronoun_subjective(self, **kwargs):
self._trigger_gather()
if isinstance(list(self.elements)[0], DAObject):
return list(self.elements)[0].pronoun_subjective(**kwargs)
- return docassemble.base.functions.it_subjective(**kwargs)
+ return it_subjective(**kwargs)
person = str(kwargs.pop('person', self.get_point_of_view()))
if person in ('2', '2p'):
- output = docassemble.base.functions.you_subjective_plural(**kwargs)
+ output = you_subjective_plural(**kwargs)
elif person in ('1', '1p'):
- output = docassemble.base.functions.we_subjective(**kwargs)
+ output = we_subjective(**kwargs)
else:
- output = docassemble.base.functions.they_subjective(**kwargs)
+ output = they_subjective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize_func(output)
return output
@@ -6238,7 +6380,7 @@ def convert_to(self, output_extension, output_to=None):
if hasattr(self, 'extension'):
input_extension = self.extension
elif hasattr(self, 'filename'):
- input_extension, input_mimetype = server.get_ext_and_mimetype(self.filename) # pylint: disable=assignment-from-none,unpacking-non-sequence,unused-variable
+ input_extension, input_mimetype = get_ext_and_mimetype(self.filename) # pylint: disable=assignment-from-none,unpacking-non-sequence,unused-variable
else:
raise DAError("DAFile.convert: could not identify file type")
output_extension = output_extension.strip().lower()
@@ -6257,13 +6399,13 @@ def convert_to(self, output_extension, output_to=None):
if input_extension == output_extension:
shutil.copyfile(input_path, output_to.path())
elif input_extension in ("docx", "doc", "odt", "rtf", "png", "jpg", "tif") and output_extension == "pdf":
- shutil.copyfile(docassemble.base.pandoc.concatenate_files([input_path]), output_to.path())
+ shutil.copyfile(concatenate_files([input_path]), output_to.path())
elif input_extension in ("docx", "doc", "odt", "rtf") and output_extension in ("docx", "doc", "odt", "rtf"):
- if not docassemble.base.pandoc.convert_file(input_path, output_to.path(), input_extension, output_extension):
+ if not convert_file(input_path, output_to.path(), input_extension, output_extension):
raise DAError("Could not convert file")
elif input_extension in ("docx", "doc", "odt", "rtf") and output_extension == 'md':
if can_convert_word_to_markdown():
- result = docassemble.base.pandoc.word_to_markdown(input_path, input_extension)
+ result = word_to_markdown(input_path, input_extension)
else:
result = None
if result is None:
@@ -6291,7 +6433,7 @@ def fix_up(self):
self.initialized # pylint: disable=pointless-statement
if hasattr(self, 'extension'):
if self.extension == 'pdf':
- docassemble.base.pdftk.apply_qpdf(self.path())
+ apply_qpdf(self.path())
elif self.extension == 'gif':
fix_gif(self.path())
elif self.extension == 'png':
@@ -6361,7 +6503,7 @@ def initialize(self, **kwargs):
if hasattr(self, 'filename'):
del self.filename
if hasattr(self, 'number'):
- server.SavedFile(self.number).delete()
+ get_saved_file_class()(self.number).delete()
del self.number
self.ok = False
if hasattr(self, 'initialized'):
@@ -6389,13 +6531,13 @@ def initialize(self, **kwargs):
self.number = kwargs['number']
self.ok = True
if hasattr(self, 'extension'):
- self.extension = server.secure_filename(self.extension)
+ self.extension = secure_filename(self.extension)
if not hasattr(self, 'filename'):
if hasattr(self, 'extension'):
self.filename = kwargs.get('filename', 'file.' + self.extension)
else:
self.filename = kwargs.get('filename', 'file.txt')
- self.filename = server.secure_filename_unicode_ok(self.filename)
+ self.filename = secure_filename_unicode_ok(self.filename)
if self.filename == '':
if hasattr(self, 'extension'):
self.filename = 'file.' + self.extension
@@ -6406,17 +6548,17 @@ def initialize(self, **kwargs):
else:
yaml_filename = None
uid = None
- if hasattr(docassemble.base.functions.this_thread, 'current_info'):
- yaml_filename = docassemble.base.functions.this_thread.current_info.get('yaml_filename', None)
- uid = docassemble.base.functions.get_uid()
- self.number = server.get_new_file_number(uid, server.secure_filename_spaces_ok(self.filename) or 'file.txt', yaml_file_name=yaml_filename) # pylint: disable=assignment-from-none
+ if hasattr(this_thread, 'current_info'):
+ yaml_filename = this_thread.current_info.get('yaml_filename', None)
+ uid = get_uid()
+ self.number = get_new_file_number(uid, secure_filename_spaces_ok(self.filename) or 'file.txt', yaml_file_name=yaml_filename) # pylint: disable=assignment-from-none
self.ok = True
- self.extension, self.mimetype = server.get_ext_and_mimetype(self.filename) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ self.extension, self.mimetype = get_ext_and_mimetype(self.filename) # pylint: disable=assignment-from-none,unpacking-non-sequence
should_not_exist = True
self.retrieve()
the_path = self.path()
if not (os.path.isfile(the_path) or os.path.islink(the_path)):
- sf = server.SavedFile(self.number, extension=self.extension, fix=True, should_not_exist=should_not_exist)
+ sf = get_saved_file_class()(self.number, extension=self.extension, fix=True, should_not_exist=should_not_exist)
sf.save()
self.initialized = True
@@ -6430,12 +6572,12 @@ def retrieve(self):
self.initialize()
if not hasattr(self, 'number'):
raise DAError("Cannot retrieve a file without a file number.")
- docassemble.base.functions.this_thread.open_files.add(self)
+ this_thread.open_files.add(self)
# logmessage("Retrieve: calling file finder")
if self.has_specific_filename:
- self.file_info = server.file_number_finder(self.number, filename=self.filename)
+ self.file_info = file_number_finder(self.number, filename=self.filename)
else:
- self.file_info = server.file_number_finder(self.number)
+ self.file_info = file_number_finder(self.number)
if self.file_info is None:
raise DAError("Could not retrieve file " + str(self.number))
if 'path' not in self.file_info:
@@ -6557,7 +6699,7 @@ def extract_pages(self, first=None, last=None, output_to=None):
input_path = temp_file.name
output_to.initialize(extension='pdf', filename=input_filename, reinitialize=output_to.ok)
try:
- docassemble.base.pdftk.extract_pages(input_path, output_to.path(), first, last)
+ extract_pages(input_path, output_to.path(), first, last)
except BaseException as err:
raise DAError("extract_pages: " + str(err))
output_to.retrieve()
@@ -6620,7 +6762,7 @@ def bates_number(self, *pargs, **kwargs):
raise DAError("bates_number: area must be one of TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, or BOTTOM_LEFT")
if filename is None:
filename = 'file.pdf'
- args = [os.path.join(server.daconfig['modules'], 'bin', 'python'), '-m', 'docassemble.base.bates', '--prefix', str(prefix), '--digits', str(digits), '--start', str(start), '--area', area, '--font-size', str(font_size), '--offset-horizontal', str(offset_horizontal), '--offset-vertical', str(offset_vertical)]
+ args = [os.path.join(get_configuration()['modules'], 'bin', 'python'), '-m', 'docassemble.base.bates', '--prefix', str(prefix), '--digits', str(digits), '--start', str(start), '--area', area, '--font-size', str(font_size), '--offset-horizontal', str(offset_horizontal), '--offset-vertical', str(offset_vertical)]
for doc in docs:
if isinstance(doc, str):
args.append(doc)
@@ -6673,8 +6815,9 @@ def make_ocr_pdf_in_background(self, *pargs, **kwargs):
AsyncResult: A Celery chord handle for the background task.
"""
lang = get_ocr_language(kwargs.get('language', None))
- args = {'yaml_filename': docassemble.base.functions.this_thread.current_info['yaml_filename'], 'user': docassemble.base.functions.this_thread.current_info['user'], 'user_code': docassemble.base.functions.this_thread.current_info['session'], 'secret': docassemble.base.functions.this_thread.current_info['secret'], 'url': docassemble.base.functions.this_thread.current_info['url'], 'url_root': docassemble.base.functions.this_thread.current_info['url_root'], 'language': lang, 'psm': kwargs.get('psm', None), 'x': None, 'y': None, 'W': None, 'H': None, 'extra': None, 'message': None, 'pdf': True, 'preserve_color': kwargs.get('preserve_color', False), 'target': self, 'dafilelist': kwargs.get('dafilelist', None), 'filename': kwargs.get('filename', None)}
- collector = server.ocr_finalize.s(**args)
+ args = {'yaml_filename': this_thread.current_info['yaml_filename'], 'user': this_thread.current_info['user'], 'user_code': this_thread.current_info['session'], 'secret': this_thread.current_info['secret'], 'url': this_thread.current_info['url'], 'url_root': this_thread.current_info['url_root'], 'language': lang, 'psm': kwargs.get('psm', None), 'x': None, 'y': None, 'W': None, 'H': None, 'extra': None, 'message': None, 'pdf': True, 'preserve_color': kwargs.get('preserve_color', False), 'target': self, 'dafilelist': kwargs.get('dafilelist', None), 'filename': kwargs.get('filename', None)}
+ celery_app = get_celery_app()
+ collector = celery_app.signature('tasks.ocr_finalize', kwargs=args)
docs = []
for parg in pargs:
if isinstance(parg, DAFileList):
@@ -6687,28 +6830,27 @@ def make_ocr_pdf_in_background(self, *pargs, **kwargs):
indexno = 0
for image_file in docs:
if hasattr(image_file, 'extension') and image_file.extension in ('docx', 'doc', 'odt', 'rtf'):
- todo.append(server.ocr_dummy.s(image_file, indexno, **args))
+ todo.append(celery_app.signature('tasks.ocr_dummy', args=[image_file, indexno], kwargs=args))
indexno += 1
elif hasattr(image_file, 'extension') and image_file._is_pdf() and hasattr(image_file, 'has_ocr') and image_file.has_ocr:
- todo.append(server.ocr_dummy.s(image_file, indexno, **args))
+ todo.append(celery_app.signature('tasks.ocr_dummy', args=[image_file, indexno], kwargs=args))
indexno += 1
else:
for item in ocr_page_tasks(image_file, **args):
- todo.append(server.ocr_page.s(indexno, **item))
+ todo.append(celery_app.signature('tasks.ocr_page', args=[indexno], kwargs=item))
indexno += 1
if len(todo) == 0:
if hasattr(self, 'extension') and self.extension in ('docx', 'doc', 'odt', 'rtf'):
- todo.append(server.ocr_dummy.s(self, indexno, **args))
+ todo.append(celery_app.signature('tasks.ocr_dummy', args=[self, indexno], kwargs=args))
indexno += 1
elif self._is_pdf() and hasattr(self, 'has_ocr') and self.has_ocr:
- todo.append(server.ocr_dummy.s(self, indexno, **args))
+ todo.append(celery_app.signature('tasks.ocr_dummy', args=[self, indexno], kwargs=args))
indexno += 1
else:
for item in ocr_page_tasks(self, **args):
- todo.append(server.ocr_page.s(indexno, **item))
+ todo.append(celery_app.signature('tasks.ocr_page', args=[indexno], kwargs=item))
indexno += 1
- the_chord = server.chord(todo)(collector) # pylint: disable=assignment-from-none
- return the_chord
+ return chord(todo)(collector) # pylint: disable=assignment-from-none
def _is_pdf(self):
if hasattr(self, 'extension') and self.extension.lower() == 'pdf':
@@ -6723,7 +6865,7 @@ def get_docx_variables(self):
Returns:
list[str]: Variable names referenced in the document template.
"""
- return docassemble.base.parse.get_docx_variables(self.path())
+ return get_docx_variables(self.path())
def get_pdf_fields(self):
"""Return a list of form fields found in the PDF document.
@@ -6733,7 +6875,7 @@ def get_pdf_fields(self):
position, page number, field type, and flags.
"""
results = []
- all_items = docassemble.base.pdftk.read_fields(self.path())
+ all_items = read_fields(self.path())
if all_items is not None:
for item in all_items:
the_type = re.sub(r'[^/A-Za-z]', '', str(item[4]))
@@ -6788,11 +6930,11 @@ def _make_pdf_thumbnail(self, page, both_formats=False):
formatter = '%0' + str(len(str(max_pages))) + 'd'
the_path = self.file_info['path'] + 'screen-' + (formatter % int(page)) + '.png'
if not os.path.isfile(the_path):
- server.fg_make_png_for_pdf(self, 'screen', page=page)
+ fg_make_png_for_pdf(self, 'screen', page=page)
if both_formats:
the_path = self.file_info['path'] + 'page-' + (formatter % int(page)) + '.png'
if not os.path.isfile(the_path):
- server.fg_make_png_for_pdf(self, 'page', page=page)
+ fg_make_png_for_pdf(self, 'page', page=page)
def pngs_ready(self):
"""Return True if the PNG page images for the PDF have been generated.
@@ -6801,7 +6943,7 @@ def pngs_ready(self):
bool: True if all PNG images are ready; False otherwise.
"""
self._make_pngs_for_pdf()
- if server.task_ready(self._taskscreen) and server.task_ready(self._taskpage):
+ if task_ready(self._taskscreen) and task_ready(self._taskpage):
return True
return False
@@ -6819,7 +6961,7 @@ def _delete_pngs(self):
self.commit()
self.retrieve()
if os.path.isfile(test_path) and hasattr(self, '_task' + prefix):
- server.wait_for_task(getattr(self, '_task' + prefix), timeout=10)
+ wait_for_task(getattr(self, '_task' + prefix), timeout=10)
self.commit()
self.retrieve()
if os.path.isfile(test_path):
@@ -6845,9 +6987,9 @@ def _delete_pngs(self):
def _make_pngs_for_pdf(self):
if not hasattr(self, '_taskscreen'):
- setattr(self, '_taskscreen', server.make_png_for_pdf(self, 'screen'))
+ setattr(self, '_taskscreen', server_make_png_for_pdf(self, 'screen'))
if not hasattr(self, '_taskpage'):
- setattr(self, '_taskpage', server.make_png_for_pdf(self, 'page'))
+ setattr(self, '_taskpage', server_make_png_for_pdf(self, 'page'))
def num_pages(self):
"""Return the number of pages in the file.
@@ -6936,7 +7078,7 @@ def page_path(self, page, prefix, wait=True):
if wait:
tries = 4
while tries > 0:
- server.wait_for_task(getattr(self, '_task' + prefix))
+ wait_for_task(getattr(self, '_task' + prefix))
self.commit()
self.retrieve()
if self._path_ready(the_path):
@@ -6950,14 +7092,14 @@ def page_path(self, page, prefix, wait=True):
if wait:
tries = 4
while tries > 0:
- server.wait_for_task(getattr(self, '_task' + prefix))
+ wait_for_task(getattr(self, '_task' + prefix))
self.commit()
self.retrieve()
if self._path_ready(the_path):
return the_path
tries -= 1
if wait:
- server.fg_make_png_for_pdf(self, prefix, page=page)
+ fg_make_png_for_pdf(self, prefix, page=page)
if os.path.isfile(the_path):
return the_path
return None
@@ -6979,7 +7121,7 @@ def cloud_path(self, filename=None):
self.initialized # pylint: disable=pointless-statement
if not hasattr(self, 'number'):
raise DAError("Cannot get the cloud path of file without a file number.")
- return server.SavedFile(self.number, fix=False).cloud_path(filename)
+ return get_saved_file_class()(self.number, fix=False).cloud_path(filename)
def path(self):
"""Return the filesystem path at which the file can be accessed.
@@ -7005,7 +7147,7 @@ def path(self):
def commit(self):
"""Persist any changes to the file so they are available in the future."""
if hasattr(self, 'number'):
- sf = server.SavedFile(self.number, fix=True)
+ sf = get_saved_file_class()(self.number, fix=True)
sf.finalize()
def show(self, width=None, wait=True, alt_text=None):
@@ -7034,12 +7176,12 @@ def show(self, width=None, wait=True, alt_text=None):
return the_content
if alt_text is None:
alt_text = self.get_alt_text()
- if docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if this_thread.evaluation_context == 'docx':
if self.mimetype == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
return docassemble.base.file_docx.include_docx_template(self, _use_jinja2=False)
if self.mimetype in ('application/pdf', 'application/rtf', 'application/vnd.oasis.opendocument.text', 'application/msword'):
return self._pdf_pages(width)
- return docassemble.base.file_docx.image_for_docx(self.number, docassemble.base.functions.this_thread.current_question, docassemble.base.functions.this_thread.misc.get('docx_template', None), width=width, alt_text=alt_text)
+ return docassemble.base.file_docx.image_for_docx(self.number, this_thread.current_question, this_thread.misc.get('docx_template', None), width=width, alt_text=alt_text)
if width is not None:
the_width = str(width)
else:
@@ -7051,7 +7193,7 @@ def show(self, width=None, wait=True, alt_text=None):
return '[FILE ' + str(self.number) + ', ' + the_width + ', ' + the_alt_text + ']'
def _pdf_pages(self, width):
- file_info = server.file_finder(self.number, question=docassemble.base.functions.this_thread.current_question)
+ file_info = file_finder(self.number, question=this_thread.current_question)
if 'path' not in file_info:
return ''
return docassemble.base.file_docx.pdf_pages(file_info, width)
@@ -7074,7 +7216,7 @@ def url_for(self, **kwargs):
if kwargs.get('attachment', False):
kwargs['_attachment'] = True
del kwargs['attachment']
- return server.url_finder(self, **kwargs)
+ return url_finder(self, **kwargs)
def set_attributes(self, **kwargs):
"""Set server-side attributes for the file.
@@ -7088,17 +7230,17 @@ def set_attributes(self, **kwargs):
if 'persistent' in kwargs and kwargs['persistent'] in [True, False]:
self.persistent = kwargs['persistent']
if 'filename' in kwargs:
- self.filename = server.secure_filename_unicode_ok(kwargs['filename'])
+ self.filename = secure_filename_unicode_ok(kwargs['filename'])
if kwargs['filename'] == '':
if hasattr(self, 'extension'):
- self.extension = server.secure_filename(self.extension)
+ self.extension = secure_filename(self.extension)
self.filename = 'file.' + self.extension
else:
self.filename = 'file.txt'
- kwargs['filename'] = server.secure_filename_spaces_ok(self.filename)
+ kwargs['filename'] = secure_filename_spaces_ok(self.filename)
if 'session' in kwargs:
del kwargs['session']
- return server.file_set_attributes(self.number, **kwargs)
+ return file_set_attributes(self.number, **kwargs)
def user_access(self, *pargs, **kwargs):
"""Grant or revoke access to the file for specific users.
@@ -7141,7 +7283,7 @@ def user_access(self, *pargs, **kwargs):
disallow_user_id.append(item)
elif isinstance(item, str):
disallow_email.append(item)
- return server.file_user_access(self.number, allow_user_id=allow_user_id, allow_email=allow_email, disallow_user_id=disallow_user_id, disallow_email=disallow_email, disallow_all=disallow_all)
+ return file_user_access(self.number, allow_user_id=allow_user_id, allow_email=allow_email, disallow_user_id=disallow_user_id, disallow_email=disallow_email, disallow_all=disallow_all)
def privilege_access(self, *pargs, **kwargs):
"""Grant or revoke access to the file for users with specific privileges.
@@ -7169,7 +7311,7 @@ def privilege_access(self, *pargs, **kwargs):
for item in disallow_arg:
if isinstance(item, str):
disallow.append(item)
- return server.file_privilege_access(self.number, allow=allow, disallow=disallow, disallow_all=disallow_all)
+ return file_privilege_access(self.number, allow=allow, disallow=disallow, disallow_all=disallow_all)
class DAFileCollection(DAObject):
@@ -7721,13 +7863,13 @@ class DAStaticFile(DAObject):
def init(self, *pargs, **kwargs):
if 'filename' in kwargs and 'mimetype' not in kwargs and 'extension' not in kwargs:
- self.extension, self.mimetype = server.get_ext_and_mimetype(kwargs['filename']) # pylint: disable=assignment-from-none,unpacking-non-sequence
- self.package = docassemble.base.functions.this_thread.current_question.package
+ self.extension, self.mimetype = get_ext_and_mimetype(kwargs['filename']) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ self.package = this_thread.current_question.package
super().init(*pargs, **kwargs)
def _populate(self):
if not hasattr(self, 'extension') or not hasattr(self, 'mimetype'):
- self.extension, self.mimetype = server.get_ext_and_mimetype(self.filename) # pylint: disable=assignment-from-none,unpacking-non-sequence
+ self.extension, self.mimetype = get_ext_and_mimetype(self.filename) # pylint: disable=assignment-from-none,unpacking-non-sequence
def get_alt_text(self):
"""Return the alternative text for the file, or None if not set.
@@ -7770,12 +7912,12 @@ def show(self, width=None, alt_text=None):
self._populate()
if alt_text is None:
alt_text = self.get_alt_text()
- if docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if this_thread.evaluation_context == 'docx':
if self.mimetype == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
return docassemble.base.file_docx.include_docx_template(self)
if self.mimetype in ('application/pdf', 'application/rtf', 'application/vnd.oasis.opendocument.text', 'application/msword'):
return self._pdf_pages(width)
- the_text = docassemble.base.file_docx.image_for_docx(docassemble.base.functions.DALocalFile(self.path()), docassemble.base.functions.this_thread.current_question, docassemble.base.functions.this_thread.misc.get('docx_template', None), width=width, alt_text=alt_text)
+ the_text = docassemble.base.file_docx.image_for_docx(DALocalFile(self.path()), this_thread.current_question, this_thread.misc.get('docx_template', None), width=width, alt_text=alt_text)
return the_text
if width is not None:
the_width = str(width)
@@ -7802,7 +7944,7 @@ def uses_acroform(self):
Returns:
bool: True if the file uses AcroForm; False otherwise.
"""
- file_info = server.file_finder(self._get_unqualified_reference())
+ file_info = file_finder(self._get_unqualified_reference())
return file_info.get('acroform', False)
def is_encrypted(self):
@@ -7811,7 +7953,7 @@ def is_encrypted(self):
Returns:
bool: True if the file is an encrypted PDF; False otherwise.
"""
- file_info = server.file_finder(self._get_unqualified_reference())
+ file_info = file_finder(self._get_unqualified_reference())
return file_info.get('encrypted', False)
def size_in_bytes(self):
@@ -7853,7 +7995,7 @@ def path(self):
str or None: Absolute filesystem path to the file, or None if not
found.
"""
- file_info = server.file_finder(self._get_unqualified_reference())
+ file_info = file_finder(self._get_unqualified_reference())
return file_info.get('fullpath', None)
def get_docx_variables(self):
@@ -7862,7 +8004,7 @@ def get_docx_variables(self):
Returns:
list[str]: Variable names referenced in the document template.
"""
- return docassemble.base.parse.get_docx_variables(self.path())
+ return get_docx_variables(self.path())
def get_pdf_fields(self):
"""Return a list of form fields found in the PDF document.
@@ -7872,7 +8014,7 @@ def get_pdf_fields(self):
position, page number, field type, and flags.
"""
results = []
- all_items = docassemble.base.pdftk.read_fields(self.path())
+ all_items = read_fields(self.path())
if all_items is not None:
for item in all_items:
the_type = re.sub(r'[^/A-Za-z]', '', str(item[4]))
@@ -7902,8 +8044,8 @@ def url_for(self, **kwargs):
if 'attachment' in kwargs:
the_args['_attachment'] = kwargs['attachment']
del the_args['attachment']
- the_args['question'] = docassemble.base.functions.this_thread.current_question
- return server.url_finder(self._get_unqualified_reference(), **the_args)
+ the_args['question'] = this_thread.current_question
+ return url_finder(self._get_unqualified_reference(), **the_args)
def _is_pdf(self):
if hasattr(self, 'extension') and self.extension.lower() == 'pdf':
@@ -7982,7 +8124,7 @@ def __str__(self):
return 'EMAIL NOT DEFINED'
if self.address == '' and name != '':
return name
- if docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if this_thread.evaluation_context == 'docx':
return str(self.address)
if name == '' and self.address != '':
return '[' + str(self.address) + '](mailto:' + str(self.address) + ')'
@@ -8049,10 +8191,10 @@ def show_as_markdown(self, **kwargs): # pylint: disable=unused-argument
return str(self.content)
def __str__(self):
- if docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if this_thread.evaluation_context == 'docx':
content = self.content
content = re.sub(r'\\_', r'\\\\_', content)
- return str(docassemble.base.file_docx.markdown_to_docx(content, docassemble.base.functions.this_thread.current_question, docassemble.base.functions.this_thread.misc.get('docx_template', None)))
+ return str(markdown_to_docx(content, this_thread.current_question, this_thread.misc.get('docx_template', None)))
return str(self.content)
@@ -8097,7 +8239,7 @@ def text_of_table(table_info, orig_user_dict, temp_vars, editable=True):
the_iterable = the_iterable.complete_elements()
elif table_info.show_incomplete and the_iterable.gathering_started():
the_iterable = the_iterable.elements
- elif docassemble.base.functions.get_gathering_mode(the_iterable.instanceName):
+ elif get_gathering_mode(the_iterable.instanceName):
the_iterable = the_iterable.complete_elements()
contents = []
if hasattr(the_iterable, 'items') and callable(the_iterable.items):
@@ -8222,17 +8364,17 @@ def subject_as_html(self, **kwargs):
the_args = {}
for key, val in kwargs.items():
the_args[key] = val
- the_args['status'] = docassemble.base.functions.this_thread.interview_status
- the_args['question'] = docassemble.base.functions.this_thread.current_question
- return docassemble.base.filter.markdown_to_html(self.subject, **the_args)
+ the_args['status'] = this_thread.interview_status
+ the_args['question'] = this_thread.current_question
+ return markdown_to_html(self.subject, **the_args)
def content_as_html(self, **kwargs):
the_args = {}
for key, val in kwargs.items():
the_args[key] = val
- the_args['status'] = docassemble.base.functions.this_thread.interview_status
- the_args['question'] = docassemble.base.functions.this_thread.current_question
- return docassemble.base.filter.markdown_to_html(self.content, **the_args)
+ the_args['status'] = this_thread.interview_status
+ the_args['question'] = this_thread.current_question
+ return markdown_to_html(self.content, **the_args)
@property
def subject(self):
@@ -8266,9 +8408,9 @@ def show(self, **kwargs):
user_dict_copy.update(self.tempvars)
user_dict_copy.update(kwargs)
content = self.source_content.text(user_dict_copy).rstrip()
- if docassemble.base.functions.this_thread.evaluation_context == 'docx' and server.daconfig.get('new template markdown behavior', False):
+ if this_thread.evaluation_context == 'docx' and get_configuration().get('new template markdown behavior', False):
content = re.sub(r'\\_', r'\\\\_', content)
- return str(docassemble.base.file_docx.markdown_to_docx(content, docassemble.base.functions.this_thread.current_question, docassemble.base.functions.this_thread.misc.get('docx_template', None)))
+ return str(markdown_to_docx(content, this_thread.current_question, this_thread.misc.get('docx_template', None)))
return content
def show_as_markdown(self, **kwargs):
@@ -8281,10 +8423,10 @@ def show_as_markdown(self, **kwargs):
return self.source_content.text(user_dict_copy).rstrip()
def __str__(self):
- if docassemble.base.functions.this_thread.evaluation_context == 'docx' and server.daconfig.get('new template markdown behavior', False):
+ if this_thread.evaluation_context == 'docx' and get_configuration().get('new template markdown behavior', False):
content = self.content
content = re.sub(r'\\_', r'\\\\_', content)
- return str(docassemble.base.file_docx.markdown_to_docx(content, docassemble.base.functions.this_thread.current_question, docassemble.base.functions.this_thread.misc.get('docx_template', None)))
+ return str(markdown_to_docx(content, this_thread.current_question, this_thread.misc.get('docx_template', None)))
return str(self.content)
@@ -8293,7 +8435,7 @@ class DALazyTableTemplate(DALazyTemplate):
def show(self, **kwargs):
"""Displays the contents of the table."""
- if docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if this_thread.evaluation_context == 'docx':
return word("ERROR: you cannot insert a table into a .docx document")
if kwargs.get('editable', True):
return str(self.content)
@@ -8376,7 +8518,7 @@ def header_and_contents(self):
the_iterable = the_iterable.complete_elements()
elif self.table_info.show_incomplete and the_iterable.gathering_started():
the_iterable = the_iterable.elements
- elif docassemble.base.functions.get_gathering_mode(the_iterable.instanceName):
+ elif get_gathering_mode(the_iterable.instanceName):
the_iterable = the_iterable.complete_elements()
contents = []
if hasattr(the_iterable, 'items') and callable(the_iterable.items):
@@ -8551,8 +8693,8 @@ def objects_from_data(data, recursive=True, gathered=True, name=None, package=No
del frame
else:
thename = name
- if package is None and docassemble.base.functions.this_thread.current_question is not None:
- package = docassemble.base.functions.this_thread.current_question.package
+ if package is None and this_thread.current_question is not None:
+ package = this_thread.current_question.package
if thename is None:
objects = DAList('objects')
objects.set_random_instance_name()
@@ -8622,9 +8764,9 @@ def objects_from_file(file_ref, recursive=True, gathered=True, name=None, use_ob
else:
thename = name
# logmessage("objects_from_file: thename is " + str(thename))
- if package is None and docassemble.base.functions.this_thread.current_question is not None:
- package = docassemble.base.functions.this_thread.current_question.package
- file_info = server.file_finder(file_ref, folder='sources', package=package)
+ if package is None and this_thread.current_question is not None:
+ package = this_thread.current_question.package
+ file_info = file_finder(file_ref, folder='sources', package=package)
if file_info is None or 'path' not in file_info:
raise SystemError('objects_from_file: file reference ' + str(file_ref) + ' not found')
if thename is None:
@@ -8701,7 +8843,7 @@ def recurse_obj(the_object, recursive=True, use_objects=False):
if not constructor:
if 'module' in the_object:
if the_object['module'].startswith('.'):
- module_name = docassemble.base.functions.this_thread.current_package + the_object['module']
+ module_name = this_thread.current_package + the_object['module']
else:
module_name = the_object['module']
new_module = __import__(module_name, globals(), locals(), [the_object['object']], 0)
@@ -8772,8 +8914,8 @@ def show(self):
str or docx Run: A DOCX hyperlink object when evaluated inside a
DOCX template; a Markdown hyperlink string otherwise.
"""
- if docassemble.base.functions.this_thread.evaluation_context == 'docx':
- return docassemble.base.file_docx.create_hyperlink(self.url, self.anchor_text, docassemble.base.functions.this_thread.misc.get('docx_template', None))
+ if this_thread.evaluation_context == 'docx':
+ return docassemble.base.file_docx.create_hyperlink(self.url, self.anchor_text, this_thread.misc.get('docx_template', None))
return '[%s](%s)' % (self.anchor_text, self.url)
@@ -8808,10 +8950,10 @@ def init(self, *pargs, **kwargs):
self[key] = val
def __str__(self):
- if isinstance(docassemble.base.functions.this_thread.evaluation_context, str) and docassemble.base.functions.this_thread.evaluation_context.startswith('pandoc'):
+ if isinstance(this_thread.evaluation_context, str) and this_thread.evaluation_context.startswith('pandoc'):
context = 'pandoc'
else:
- context = docassemble.base.functions.this_thread.evaluation_context
+ context = this_thread.evaluation_context
if context in ('docx', 'pdf', 'pandoc'):
if context in self.elements:
return str(self.elements[context])
@@ -8899,12 +9041,12 @@ class DAGlobal(DAObject):
@classmethod
def keys(cls, base):
if base == 'interview':
- globalbase = 'da:daglobal:i:' + str(docassemble.base.functions.this_thread.current_info.get('yaml_filename', ''))
+ globalbase = 'da:daglobal:i:' + str(this_thread.current_info.get('yaml_filename', ''))
elif base == 'global':
globalbase = 'da:daglobal:global'
else:
- globalbase = 'da:daglobal:userid:' + str(docassemble.base.functions.this_thread.current_info['user']['the_user_id'])
- return server.server_sql_keys(globalbase + ':')
+ globalbase = 'da:daglobal:userid:' + str(this_thread.current_info['user']['the_user_id'])
+ return server_sql_keys(globalbase + ':')
@classmethod
def defined(cls, base, key):
@@ -8919,12 +9061,12 @@ def defined(cls, base, key):
bool: True if the key exists; False otherwise.
"""
if base == 'interview':
- globalkey = 'da:daglobal:i:' + str(docassemble.base.functions.this_thread.current_info.get('yaml_filename', '')) + ':' + str(key)
+ globalkey = 'da:daglobal:i:' + str(this_thread.current_info.get('yaml_filename', '')) + ':' + str(key)
elif base == 'global':
globalkey = 'da:daglobal:global:' + str(key)
else:
- globalkey = 'da:daglobal:userid:' + str(docassemble.base.functions.this_thread.current_info['user']['the_user_id']) + ':' + str(key)
- return server.server_sql_defined(globalkey)
+ globalkey = 'da:daglobal:userid:' + str(this_thread.current_info['user']['the_user_id']) + ':' + str(key)
+ return server_sql_defined(globalkey)
@classmethod
def remove(cls, base, key):
@@ -8936,12 +9078,12 @@ def remove(cls, base, key):
key (str): The key to delete.
"""
if base == 'interview':
- globalkey = 'da:daglobal:i:' + str(docassemble.base.functions.this_thread.current_info.get('yaml_filename', '')) + ':' + str(key)
+ globalkey = 'da:daglobal:i:' + str(this_thread.current_info.get('yaml_filename', '')) + ':' + str(key)
elif base == 'global':
globalkey = 'da:daglobal:global:' + str(key)
else:
- globalkey = 'da:daglobal:userid:' + str(docassemble.base.functions.this_thread.current_info['user']['the_user_id']) + ':' + str(key)
- server.server_sql_delete(globalkey)
+ globalkey = 'da:daglobal:userid:' + str(this_thread.current_info['user']['the_user_id']) + ':' + str(key)
+ server_sql_delete(globalkey)
def init(self, *pargs, **kwargs):
super().init(*pargs, **kwargs)
@@ -8950,12 +9092,12 @@ def init(self, *pargs, **kwargs):
if 'key' not in kwargs:
self.key = random_alphanumeric(32)
if self.base == 'interview':
- globalkey = 'da:daglobal:i:' + str(docassemble.base.functions.this_thread.current_info.get('yaml_filename', '')) + ':' + str(self.key)
+ globalkey = 'da:daglobal:i:' + str(this_thread.current_info.get('yaml_filename', '')) + ':' + str(self.key)
elif self.base == 'global':
globalkey = 'da:daglobal:global:' + str(self.key)
else:
- globalkey = 'da:daglobal:userid:' + str(docassemble.base.functions.this_thread.current_info['user']['the_user_id']) + ':' + str(self.key)
- saved_dict = server.server_sql_get(globalkey) # pylint: disable=assignment-from-none
+ globalkey = 'da:daglobal:userid:' + str(this_thread.current_info['user']['the_user_id']) + ':' + str(self.key)
+ saved_dict = server_sql_get(globalkey) # pylint: disable=assignment-from-none
if isinstance(saved_dict, dict):
for key, val in saved_dict.items():
setattr(self, key, val)
@@ -8963,11 +9105,11 @@ def init(self, *pargs, **kwargs):
def __getstate__(self):
if hasattr(self, 'base') and hasattr(self, 'key'):
if self.base == 'interview':
- globalkey = 'da:daglobal:i:' + str(docassemble.base.functions.this_thread.current_info.get('yaml_filename', '')) + ':' + str(self.key)
+ globalkey = 'da:daglobal:i:' + str(this_thread.current_info.get('yaml_filename', '')) + ':' + str(self.key)
elif self.base == 'global':
globalkey = 'da:daglobal:global:' + str(self.key)
else:
- globalkey = 'da:daglobal:userid:' + str(docassemble.base.functions.this_thread.current_info['user']['the_user_id']) + ':' + str(self.key)
+ globalkey = 'da:daglobal:userid:' + str(this_thread.current_info['user']['the_user_id']) + ':' + str(self.key)
dict_to_save = copy.copy(self.__dict__)
dict_to_return = {'attrList': []}
if 'instanceName' in dict_to_save:
@@ -8982,7 +9124,7 @@ def __getstate__(self):
if 'key' in dict_to_save:
dict_to_return['key'] = dict_to_save['key']
del dict_to_save['key']
- server.server_sql_set(globalkey, dict_to_save, encrypted=False)
+ server_sql_set(globalkey, dict_to_save, encrypted=False)
return dict_to_return
dict_to_return = copy.copy(self.__dict__)
return dict_to_return
@@ -8991,13 +9133,13 @@ def __setstate__(self, pickle_dict):
self.__dict__ = pickle_dict
if 'base' in pickle_dict and 'key' in pickle_dict:
if pickle_dict['base'] == 'interview':
- globalkey = 'da:daglobal:i:' + str(docassemble.base.functions.this_thread.current_info.get('yaml_filename', '')) + ':' + str(pickle_dict['key'])
+ globalkey = 'da:daglobal:i:' + str(this_thread.current_info.get('yaml_filename', '')) + ':' + str(pickle_dict['key'])
elif pickle_dict['base'] == 'global':
globalkey = 'da:daglobal:global:' + str(pickle_dict['key'])
else:
- globalkey = 'da:daglobal:userid:' + str(docassemble.base.functions.this_thread.current_info['user']['the_user_id']) + ':' + str(pickle_dict['key'])
+ globalkey = 'da:daglobal:userid:' + str(this_thread.current_info['user']['the_user_id']) + ':' + str(pickle_dict['key'])
- saved_dict = server.server_sql_get(globalkey) # pylint: disable=assignment-from-none
+ saved_dict = server_sql_get(globalkey) # pylint: disable=assignment-from-none
if isinstance(saved_dict, dict):
for key, val in saved_dict.items():
setattr(self, key, val)
@@ -9006,12 +9148,12 @@ def delete(self):
"""Delete all data from global storage and undefine all object attributes."""
if hasattr(self, 'base') and hasattr(self, 'key'):
if self.base == 'interview':
- globalkey = 'da:daglobal:i:' + docassemble.base.functions.this_thread.current_info.get('yaml_filename', '') + ':' + self.key
+ globalkey = 'da:daglobal:i:' + this_thread.current_info.get('yaml_filename', '') + ':' + self.key
elif self.base == 'global':
globalkey = 'da:daglobal:global:' + self.key
else:
- globalkey = 'da:daglobal:userid:' + str(docassemble.base.functions.this_thread.current_info['user']['the_user_id']) + ':' + self.key
- server.server_sql_delete(globalkey)
+ globalkey = 'da:daglobal:userid:' + str(this_thread.current_info['user']['the_user_id']) + ':' + self.key
+ server_sql_delete(globalkey)
self.__dict__ = {'instanceName': self.instanceName, 'attrList': [], 'has_nonrandom_instance_name': self.has_nonrandom_instance_name}
@@ -9050,15 +9192,15 @@ def is_encrypted(self):
def _get_base_key(self):
if hasattr(self, 'base'):
if self.base == 'interview':
- return 'da:i:' + docassemble.base.functions.this_thread.current_info.get('yaml_filename', '')
+ return 'da:i:' + this_thread.current_info.get('yaml_filename', '')
if self.base == 'user':
- return 'da:userid:' + str(docassemble.base.functions.this_thread.current_info['user']['the_user_id'])
+ return 'da:userid:' + str(this_thread.current_info['user']['the_user_id'])
if self.base == 'session':
- return 'da:uid:' + get_uid() + ':i:' + docassemble.base.functions.this_thread.current_info.get('yaml_filename', '')
+ return 'da:uid:' + get_uid() + ':i:' + this_thread.current_info.get('yaml_filename', '')
if self.base == 'global':
return 'da:global'
return str(self.base)
- return 'da:userid:' + str(docassemble.base.functions.this_thread.current_info['user']['the_user_id'])
+ return 'da:userid:' + str(this_thread.current_info['user']['the_user_id'])
def defined(self, key):
"""Return True if the given key exists in the store.
@@ -9070,7 +9212,7 @@ def defined(self, key):
bool: True if the key exists; False otherwise.
"""
the_key = self._get_base_key() + ':' + key
- return server.server_sql_defined(the_key)
+ return server_sql_defined(the_key)
def get(self, key):
"""Retrieve the value stored under the given key.
@@ -9082,7 +9224,7 @@ def get(self, key):
object: The stored value, or None if not found.
"""
the_key = self._get_base_key() + ':' + key
- return server.server_sql_get(the_key, secret=docassemble.base.functions.this_thread.current_info.get('secret', None))
+ return server_sql_get(the_key, secret=this_thread.current_info.get('secret', None))
def set(self, key, the_value):
"""Store a value under the given key.
@@ -9092,7 +9234,7 @@ def set(self, key, the_value):
the_value (object): Value to store.
"""
the_key = self._get_base_key() + ':' + key
- server.server_sql_set(the_key, the_value, encrypted=self.is_encrypted(), secret=docassemble.base.functions.this_thread.current_info.get('secret', None), the_user_id=docassemble.base.functions.this_thread.current_info['user']['the_user_id'])
+ server_sql_set(the_key, the_value, encrypted=self.is_encrypted(), secret=this_thread.current_info.get('secret', None), the_user_id=this_thread.current_info['user']['the_user_id'])
def delete(self, key):
"""Delete the value stored under the given key.
@@ -9101,7 +9243,7 @@ def delete(self, key):
key (str): Key to delete.
"""
the_key = self._get_base_key() + ':' + key
- server.server_sql_delete(the_key)
+ server_sql_delete(the_key)
def keys(self):
"""Return a list of all keys currently stored.
@@ -9109,7 +9251,7 @@ def keys(self):
Returns:
list[str]: Keys present in this store.
"""
- return server.server_sql_keys(self._get_base_key() + ':')
+ return server_sql_keys(self._get_base_key() + ':')
class BearerAuth(AuthBase):
@@ -9300,7 +9442,7 @@ def _call(self, url, method=None, data=None, params=None, headers=None, json_bod
if not isinstance(key, str):
raise DAError("DAWeb.call: files must be a dictionary of string keys")
try:
- path = server.path_from_reference(val)
+ path = path_from_reference(val)
logmessage("path is " + str(path))
assert path is not None
except:
@@ -9557,7 +9699,7 @@ def key(self, keyname):
Returns:
str: Fully qualified Redis key.
"""
- return docassemble.base.functions.this_thread.current_info.get('yaml_filename', '') + ':' + str(keyname)
+ return this_thread.current_info.get('yaml_filename', '') + ':' + str(keyname)
def get_data(self, key):
"""Retrieve and unpickle a Python object stored in Redis.
@@ -9569,11 +9711,11 @@ def get_data(self, key):
object: Unpickled value, or None if the key does not exist or
unpickling fails.
"""
- result = server.server_redis_user.get(key)
+ result = get_server_redis_user().get(key)
if result is None:
return None
try:
- result = server.fix_pickle_obj(result)
+ result = fix_pickle_obj(result)
except:
logmessage("get_data: could not unpickle contents of " + str(key))
result = None
@@ -9595,15 +9737,15 @@ def set_data(self, key, data, expire=None):
if expire is not None:
if not isinstance(expire, int):
raise DAError("set_data: expire time must be an integer")
- pipe = server.server_redis_user.pipeline()
+ pipe = get_server_redis_user().pipeline()
pipe.set(key, pickled_data)
pipe.expire(key, expire)
pipe.execute()
else:
- server.server_redis_user.set(key, pickled_data)
+ get_server_redis_user().set(key, pickled_data)
def __getattr__(self, funcname):
- return getattr(server.server_redis_user, funcname)
+ return getattr(get_server_redis_user(), funcname)
class DACloudStorage(DAObject):
@@ -9623,7 +9765,7 @@ def init(self, *pargs, **kwargs):
self.config = kwargs['config']
del kwargs['provider']
del kwargs['config']
- server.cloud_custom(self.provider, self.config)
+ cloud_custom(self.provider, self.config)
else:
self.custom = False
super().init(*pargs, **kwargs)
@@ -9632,36 +9774,36 @@ def init(self, *pargs, **kwargs):
def conn(self):
"""The underlying cloud connection object (``boto3.resource('s3')`` or ``BlockBlobService``)."""
if self.custom:
- return server.cloud_custom(self.provider, self.config).conn
- return server.cloud.conn
+ return cloud_custom(self.provider, self.config).conn
+ return get_cloud().conn
@property
def client(self):
"""The ``boto3.client('s3')`` object for low-level S3 operations."""
if self.custom:
- return server.cloud_custom(self.provider, self.config).client
- return server.cloud.client
+ return cloud_custom(self.provider, self.config).client
+ return get_cloud().client
@property
def bucket(self):
"""The ``boto3 Bucket`` object for the configured S3 bucket."""
if self.custom:
- return server.cloud_custom(self.provider, self.config).bucket
- return server.cloud.bucket
+ return cloud_custom(self.provider, self.config).bucket
+ return get_cloud().bucket
@property
def bucket_name(self):
"""The name of the Amazon S3 bucket."""
if self.custom:
- return server.cloud_custom(self.provider, self.config).bucket_name
- return server.cloud.bucket_name
+ return cloud_custom(self.provider, self.config).bucket_name
+ return get_cloud().bucket_name
@property
def container_name(self):
"""The name of the Azure Blob Storage container."""
if self.custom:
- return server.cloud_custom(self.provider, self.config).container
- return server.cloud.container
+ return cloud_custom(self.provider, self.config).container
+ return get_cloud().container
class BackgroundAction(DAObject):
@@ -9738,7 +9880,7 @@ def api_credentials(self, scope):
Returns:
google.oauth2.credentials.Credentials: Authenticated credentials.
"""
- return server.google_api().google_api_credentials(scope)
+ return google_api().google_api_credentials(scope)
def http(self, scope):
"""Return an authorized ``httplib2.Http`` object for the given API scope.
@@ -9780,7 +9922,7 @@ def cloud_credentials(self, scopes=None):
google.oauth2.service_account.Credentials: Service account
credentials.
"""
- return server.google_api().google_cloud_credentials(scopes=scopes)
+ return google_api().google_cloud_credentials(scopes=scopes)
def project_id(self):
"""Return the Google Cloud project ID from the service-account credentials.
@@ -9788,7 +9930,7 @@ def project_id(self):
Returns:
str: Google Cloud project ID.
"""
- return server.google_api().project_id()
+ return google_api().project_id()
def google_cloud_storage_client(self):
"""Return an authenticated Google Cloud Storage client.
@@ -9796,7 +9938,7 @@ def google_cloud_storage_client(self):
Returns:
google.cloud.storage.Client: Authorized Cloud Storage client.
"""
- return server.google_api().google_cloud_storage_client()
+ return google_api().google_cloud_storage_client()
def google_cloud_vision_client(self):
"""Return an authenticated Google Cloud Vision client.
@@ -9804,7 +9946,7 @@ def google_cloud_vision_client(self):
Returns:
google.cloud.vision.ImageAnnotatorClient: Authorized Vision client.
"""
- return server.google_api().google_cloud_vision_client()
+ return google_api().google_cloud_vision_client()
def run_python_module(module, arguments=None):
@@ -9826,9 +9968,9 @@ def run_python_module(module, arguments=None):
DAError: If ``arguments`` is not a list.
"""
if re.search(r'\.py$', module):
- module = docassemble.base.functions.this_thread.current_package + '.' + re.sub(r'\.py$', '', module)
+ module = this_thread.current_package + '.' + re.sub(r'\.py$', '', module)
elif re.search(r'^\.', module):
- module = docassemble.base.functions.this_thread.current_package + module
+ module = this_thread.current_package + module
commands = [re.sub(r'/lib/python.*', '/bin/python3', docassemble.base.pandoc.__file__), '-m', module]
if arguments:
if not isinstance(arguments, list):
@@ -9844,475 +9986,6 @@ def run_python_module(module, arguments=None):
return output, return_code
-def today(timezone=None, format=None): # pylint: disable=redefined-builtin
- """Return today's date at midnight as a DADateTime object.
-
- Args:
- timezone (str or None): IANA timezone name. If None, the interview's
- default timezone is used.
- format (str or None): If provided, return the date formatted as a
- string using this Babel date-format pattern instead of a
- DADateTime.
-
- Returns:
- DADateTime or str: Midnight today in the given timezone, or a
- formatted date string if ``format`` is specified.
- """
- ensure_definition(timezone, format)
- if timezone is None:
- timezone = get_default_timezone()
- val = datetime.datetime.now(datetime.timezone.utc).astimezone(zoneinfo.ZoneInfo(timezone))
- if format is not None:
- return dd(val.replace(hour=0, minute=0, second=0, microsecond=0)).format_date(format)
- return dd(val.replace(hour=0, minute=0, second=0, microsecond=0))
-
-
-def babel_language(language):
- if 'babel dates map' not in server.daconfig:
- return language
- return server.daconfig['babel dates map'].get(language, language)
-
-
-def month_of(the_date, as_word=False, language=None):
- """Return the month component of a date.
-
- Args:
- the_date (datetime.date, datetime.datetime, or str): The date to
- extract the month from.
- as_word (bool): If True, return the full month name (e.g.
- ``'January'``); otherwise return the month as an integer.
- language (str or None): Language code for localizing the month name.
- Defaults to the current interview language.
-
- Returns:
- int or str: Month number (1–12) or localized month name.
- """
- ensure_definition(the_date, as_word, language)
- if language is None:
- language = get_language()
- try:
- if isinstance(the_date, (datetime.datetime, datetime.date)):
- date = the_date
- else:
- date = dateutil.parser.parse(the_date)
- if as_word:
- return babel.dates.format_date(date, format='MMMM', locale=babel_language(language))
- return int(date.strftime('%m'))
- except:
- return word("Bad date")
-
-
-def day_of(the_date, language=None):
- """Return the day-of-month component of a date.
-
- Args:
- the_date (datetime.date, datetime.datetime, or str): The date to
- extract the day from.
- language (str or None): Unused; retained for API consistency.
-
- Returns:
- int: Day of the month (1–31).
- """
- ensure_definition(the_date, language)
- try:
- if isinstance(the_date, (datetime.datetime, datetime.date)):
- date = the_date
- else:
- date = dateutil.parser.parse(the_date)
- return int(date.strftime('%d'))
- except:
- return word("Bad date")
-
-
-def dow_of(the_date, as_word=False, language=None):
- """Return the day of the week for a date.
-
- Args:
- the_date (datetime.date, datetime.datetime, or str): The date to
- inspect.
- as_word (bool): If True, return the full weekday name (e.g.
- ``'Monday'``); otherwise return an integer from 1 (Monday) to
- 7 (Sunday) per ISO 8601.
- language (str or None): Language code for localizing the weekday
- name. Defaults to the current interview language.
-
- Returns:
- int or str: Day-of-week number or localized weekday name.
- """
- ensure_definition(the_date, as_word, language)
- if language is None:
- language = get_language()
- try:
- if isinstance(the_date, (datetime.datetime, datetime.date)):
- date = the_date
- else:
- date = dateutil.parser.parse(the_date)
- if as_word:
- return babel.dates.format_date(date, format='EEEE', locale=babel_language(language))
- return int(date.strftime('%u'))
- except:
- return word("Bad date")
-
-
-def year_of(the_date, language=None):
- """Return the year component of a date.
-
- Args:
- the_date (datetime.date, datetime.datetime, or str): The date to
- extract the year from.
- language (str or None): Unused; retained for API consistency.
-
- Returns:
- int: Four-digit year.
- """
- ensure_definition(the_date, language)
- try:
- if isinstance(the_date, (datetime.datetime, datetime.date)):
- date = the_date
- else:
- date = dateutil.parser.parse(the_date)
- return int(date.strftime('%Y'))
- except:
- return word("Bad date")
-
-
-def interview_default(the_part, default_value, language):
- if the_part in docassemble.base.functions.this_thread.internal and docassemble.base.functions.this_thread.internal[the_part] is not None:
- return docassemble.base.functions.this_thread.internal[the_part]
- for lang in (language, get_language(), '*'):
- if lang is not None and docassemble.base.functions.this_thread.interview is not None and lang in docassemble.base.functions.this_thread.interview.default_title and the_part in docassemble.base.functions.this_thread.interview.default_title[lang]:
- return docassemble.base.functions.this_thread.interview.default_title[lang][the_part]
- return default_value
-
-
-def format_date(the_date, format=None, language=None): # pylint: disable=redefined-builtin
- """Return a date formatted as a localized string.
-
- Args:
- the_date (datetime.date, datetime.datetime, or str): Date to format.
- format (str or None): Babel date-format pattern (e.g. ``'long'``,
- ``'short'``, ``'MM/dd/yyyy'``). Defaults to the interview's
- configured date format or ``'long'``.
- language (str or None): Language/locale code. Defaults to the current
- interview language.
-
- Returns:
- str: Formatted date string, or ``''`` for an empty date.
- """
- ensure_definition(the_date, format, language)
- if isinstance(the_date, DAEmpty):
- return ""
- if language is None:
- language = get_language()
- if format is None:
- format = interview_default('date format', 'long', language)
- try:
- if isinstance(the_date, (datetime.datetime, datetime.date)):
- date = the_date
- else:
- date = dateutil.parser.parse(the_date)
- return babel.dates.format_date(date, format=format, locale=babel_language(language))
- except:
- return word("Bad date")
-
-
-def format_datetime(the_date, format=None, language=None): # pylint: disable=redefined-builtin
- """Return a date and time formatted as a localized string.
-
- Args:
- the_date (datetime.datetime or str): Date/time to format.
- format (str or None): Babel datetime-format pattern. Defaults to the
- interview's configured datetime format or ``'long'``.
- language (str or None): Language/locale code. Defaults to the current
- interview language.
-
- Returns:
- str: Formatted datetime string, or ``''`` for an empty date.
- """
- ensure_definition(the_date, format, language)
- if isinstance(the_date, DAEmpty):
- return ""
- if language is None:
- language = get_language()
- if format is None:
- format = interview_default('datetime format', 'long', language)
- try:
- if isinstance(the_date, (datetime.datetime, datetime.date)):
- date = the_date
- else:
- date = dateutil.parser.parse(the_date)
- return babel.dates.format_datetime(date, format=format, locale=babel_language(language))
- except:
- return word("Bad date")
-
-
-def format_time(the_time, format=None, language=None): # pylint: disable=redefined-builtin
- """Return a time formatted as a localized string.
-
- Args:
- the_time (datetime.time, datetime.datetime, or str): Time to format.
- format (str or None): Babel time-format pattern. Defaults to the
- interview's configured time format or ``'short'``.
- language (str or None): Language/locale code. Defaults to the current
- interview language.
-
- Returns:
- str: Formatted time string, or ``''`` for an empty time.
- """
- ensure_definition(the_time, format, language)
- if isinstance(the_time, DAEmpty):
- return ""
- if language is None:
- language = get_language()
- if format is None:
- format = interview_default('time format', 'short', language)
- try:
- if isinstance(the_time, (datetime.datetime, datetime.date, datetime.time)):
- this_time = the_time
- else:
- this_time = dateutil.parser.parse(the_time)
- return babel.dates.format_time(this_time, format=format, locale=babel_language(language))
- except BaseException as errmess:
- return word("Bad date: " + str(errmess))
-
-
-class DateTimeDelta:
-
- def __str__(self):
- return str(self.describe())
-
- def describe(self, **kwargs):
- specificity = kwargs.get('specificity', None)
- output = []
- diff = dateutil.relativedelta.relativedelta(self.end, self.start)
- if diff.years != 0:
- output.append((abs(diff.years), noun_plural(word('year'), abs(diff.years), noun_is_singular=True)))
- if diff.months != 0 and specificity != 'year':
- output.append((abs(diff.months), noun_plural(word('month'), abs(diff.months), noun_is_singular=True)))
- if diff.days != 0 and specificity not in ('year', 'month'):
- output.append((abs(diff.days), noun_plural(word('day'), abs(diff.days), noun_is_singular=True)))
- if len(output) == 0 or specificity in ('hour', 'minute', 'second'):
- if diff.hours != 0 and specificity not in ('year', 'month', 'day'):
- output.append((abs(diff.hours), noun_plural(word('hour'), abs(diff.hours), noun_is_singular=True)))
- if (abs(diff.hours) < 2 or specificity in ('minute', 'second')) and diff.minutes != 0 and specificity not in ('year', 'month', 'day', 'hour'):
- output.append((abs(diff.minutes), noun_plural(word('minute'), abs(diff.minutes), noun_is_singular=True)))
- if len(output) == 0 or specificity == 'second':
- if diff.seconds != 0 and specificity not in ('year', 'month', 'day', 'hour', 'minute'):
- output.append((abs(diff.seconds), noun_plural(word('second'), abs(diff.seconds), noun_is_singular=True)))
- if len(output) == 0:
- if specificity is None:
- output.append((0, noun_plural(word('second'), 0, noun_is_singular=True)))
- else:
- output.append((0, noun_plural(word(specificity), 0, noun_is_singular=True)))
- if kwargs.get('nice', True):
- return_value = comma_and_list(["%s %s" % (nice_number(y[0]), y[1]) for y in output])
- if kwargs.get('capitalize', False):
- return capitalize(return_value)
- return return_value
- return comma_and_list(["%d %s" % y for y in output])
-
-
-class DADateTime(datetime.datetime):
- """A timezone-aware datetime subclass with docassemble-specific formatting and arithmetic.
-
- Inherits all ``datetime.datetime`` behavior and adds convenience methods
- for formatting, date arithmetic, and accessing ISO calendar properties.
-
- Attributes:
- dow (int): Day of the week (1 = Monday … 7 = Sunday, ISO 8601).
- week (int): ISO week number of the year.
- nanosecond (int): Always 0; provided for compatibility.
- """
-
- def format(self, format=None, language=None): # pylint: disable=redefined-builtin
- return format_date(self, format=format, language=language)
-
- def format_date(self, format=None, language=None): # pylint: disable=redefined-builtin
- return format_date(self, format=format, language=language)
-
- def format_datetime(self, format=None, language=None): # pylint: disable=redefined-builtin
- return format_datetime(self, format=format, language=language)
-
- def format_time(self, format=None, language=None): # pylint: disable=redefined-builtin
- return format_time(self, format=format, language=language)
-
- def replace_time(self, the_time):
- return self.replace(hour=the_time.hour, minute=the_time.minute, second=the_time.second, microsecond=the_time.microsecond)
-
- @property
- def nanosecond(self):
- return 0
-
- @property
- def dow(self):
- return self.isocalendar()[2]
-
- @property
- def week(self):
- return self.isocalendar()[1]
-
- def plus(self, **kwargs):
- return dd(dt(self) + date_interval(**kwargs))
-
- def minus(self, **kwargs):
- return dd(dt(self) - date_interval(**kwargs))
-
- def __str__(self):
- return str(format_date(self))
-
- def __add__(self, other):
- if isinstance(other, str):
- return str(self) + other
- val = dt(self) + other
- if isinstance(val, datetime.date):
- return dd(val)
- return val
-
- def __radd__(self, other):
- if isinstance(other, str):
- return other + str(self)
- return dd(dt(self) + other)
-
- def __sub__(self, other):
- val = dt(self) - other
- if isinstance(val, datetime.date):
- return dd(val)
- return val
-
- def __rsub__(self, other):
- val = other - dt(self)
- if isinstance(val, datetime.date):
- return dd(val)
- return val
-
-
-def current_datetime(timezone=None):
- """Return the current date and time as a DADateTime object.
-
- Args:
- timezone (str or None): IANA timezone name. If None, the interview's
- default timezone is used.
-
- Returns:
- DADateTime: Current date and time in the specified timezone.
- """
- ensure_definition(timezone)
- if timezone is None:
- timezone = get_default_timezone()
- return dd(datetime.datetime.now(datetime.timezone.utc).astimezone(zoneinfo.ZoneInfo(timezone)))
-
-
-def as_datetime(the_date, timezone=None):
- """Convert a date or date string to a timezone-aware DADateTime object.
-
- Args:
- the_date (datetime.date, datetime.datetime, or str): Date or
- date/time value to convert. String values are parsed with
- ``dateutil``.
- timezone (str or None): IANA timezone name to attach. If the value
- already carries timezone information it is converted to this
- zone; otherwise the timezone is applied as-is. Defaults to
- the interview's default timezone.
-
- Returns:
- DADateTime: Timezone-aware datetime.
- """
- ensure_definition(the_date, timezone)
- if timezone is None:
- timezone = get_default_timezone()
- if isinstance(the_date, datetime.date) and not isinstance(the_date, datetime.datetime):
- the_date = datetime.datetime.combine(the_date, datetime.datetime.min.time())
- if isinstance(the_date, datetime.datetime):
- new_datetime = the_date
- else:
- new_datetime = dateutil.parser.parse(the_date)
- if new_datetime.tzinfo:
- new_datetime = new_datetime.astimezone(zoneinfo.ZoneInfo(timezone))
- else:
- new_datetime = new_datetime.replace(tzinfo=zoneinfo.ZoneInfo(timezone))
- return dd(new_datetime)
-
-
-def dd(obj):
- if isinstance(obj, DADateTime):
- return obj
- return DADateTime(obj.year, month=obj.month, day=obj.day, hour=obj.hour, minute=obj.minute, second=obj.second, microsecond=obj.microsecond, tzinfo=obj.tzinfo)
-
-
-def dt(obj):
- return datetime.datetime(obj.year, obj.month, obj.day, obj.hour, obj.minute, obj.second, obj.microsecond, obj.tzinfo)
-
-
-def date_interval(**kwargs):
- """Return a relative date/time interval.
-
- All keyword arguments are forwarded to
- ``dateutil.relativedelta.relativedelta``. Common arguments include
- ``years``, ``months``, ``weeks``, ``days``, ``hours``, ``minutes``,
- and ``seconds``.
-
- Returns:
- dateutil.relativedelta.relativedelta: Interval that can be added to
- or subtracted from a ``DADateTime`` or ``datetime`` object.
- """
- ensure_definition(**kwargs)
- return dateutil.relativedelta.relativedelta(**kwargs)
-
-
-def date_difference(starting=None, ending=None, timezone=None):
- """Return the difference between two dates.
-
- Args:
- starting (datetime.date, datetime.datetime, str, or None): Start of
- the interval. Defaults to the current datetime.
- ending (datetime.date, datetime.datetime, str, or None): End of the
- interval. Defaults to the current datetime.
- timezone (str or None): IANA timezone name used when localizing
- naive datetimes. Defaults to the interview's default timezone.
-
- Returns:
- DateTimeDelta: Object with ``weeks``, ``days``, ``hours``,
- ``minutes``, ``seconds``, ``years``, and ``delta`` attributes
- expressing the difference, and ``start``/``end`` attributes
- holding the resolved datetime objects.
- """
- ensure_definition(starting, ending, timezone)
- if starting is None:
- starting = current_datetime()
- if ending is None:
- ending = current_datetime()
- if timezone is None:
- timezone = get_default_timezone()
- if isinstance(starting, datetime.date) and not isinstance(starting, datetime.datetime):
- starting = datetime.datetime.combine(starting, datetime.datetime.min.time())
- if isinstance(ending, datetime.date) and not isinstance(ending, datetime.datetime):
- ending = datetime.datetime.combine(ending, datetime.datetime.min.time())
- if not isinstance(starting, datetime.datetime):
- starting = dateutil.parser.parse(starting)
- if not isinstance(ending, datetime.datetime):
- ending = dateutil.parser.parse(ending)
- if starting.tzinfo:
- starting = starting.astimezone(zoneinfo.ZoneInfo(timezone))
- else:
- starting = starting.replace(tzinfo=zoneinfo.ZoneInfo(timezone))
- if ending.tzinfo:
- ending = ending.astimezone(zoneinfo.ZoneInfo(timezone))
- else:
- ending = ending.replace(tzinfo=zoneinfo.ZoneInfo(timezone))
- delta = ending - starting
- output = DateTimeDelta()
- output.start = starting
- output.end = ending
- output.weeks = (delta.days / 7.0) + (delta.seconds / 604800.0)
- output.days = delta.days + (delta.seconds / 86400.0)
- output.hours = (delta.days * 24.0) + (delta.seconds / 3600.0)
- output.minutes = (delta.days * 1440.0) + (delta.seconds / 60.0)
- output.seconds = (delta.days * 86400) + delta.seconds
- output.years = (delta.days + delta.seconds / 86400.0) / 365.2425
- output.delta = delta
- return output
-
-
def fax_string(person, country=None):
if person is None:
return None
@@ -10398,7 +10071,7 @@ def returning_user(minutes=None, hours=None, days=None):
bool: True if the user is returning after the specified period (or
after 6 hours by default); False otherwise or on POST requests.
"""
- if docassemble.base.functions.this_thread.current_info['method'] != 'GET':
+ if this_thread.current_info['method'] != 'GET':
return False
if minutes is not None and last_access_minutes() > minutes:
return True
@@ -10496,7 +10169,7 @@ def last_access_time(include_privileges=None, exclude_privileges=None, include_c
exclude_privileges = [exclude_privileges]
else:
exclude_privileges = []
- for user_id, access_time in docassemble.base.functions.this_thread.internal['accesstime'].items():
+ for user_id, access_time in this_thread.internal['accesstime'].items():
if user_id == -1:
if 'anonymous' in exclude_privileges:
continue
@@ -10505,7 +10178,7 @@ def last_access_time(include_privileges=None, exclude_privileges=None, include_c
max_time = access_time
break
else:
- user_object = server.get_user_object(user_id)
+ user_object = get_user_object(user_id)
if user_object is not None and hasattr(user_object, 'roles'):
if len(user_object.roles) == 0:
if 'user' in exclude_privileges:
@@ -10540,8 +10213,8 @@ def start_time(timezone=None):
DADateTime: Session start time.
"""
if timezone is not None:
- return dd(docassemble.base.functions.this_thread.internal['starttime'].replace(tzinfo=datetime.timezone.utc).astimezone(zoneinfo.ZoneInfo(timezone)))
- return dd(docassemble.base.functions.this_thread.internal['starttime'].replace(tzinfo=datetime.timezone.utc))
+ return dd(this_thread.internal['starttime'].replace(tzinfo=datetime.timezone.utc).astimezone(zoneinfo.ZoneInfo(timezone)))
+ return dd(this_thread.internal['starttime'].replace(tzinfo=datetime.timezone.utc))
class LatitudeLongitude(DAObject):
@@ -10583,14 +10256,14 @@ def status(self):
def _set_to_current(self):
# logmessage("set to current")
- if 'user' in docassemble.base.functions.this_thread.current_info and 'location' in docassemble.base.functions.this_thread.current_info['user'] and isinstance(docassemble.base.functions.this_thread.current_info['user']['location'], dict):
- if 'latitude' in docassemble.base.functions.this_thread.current_info['user']['location'] and 'longitude' in docassemble.base.functions.this_thread.current_info['user']['location']:
- self.latitude = docassemble.base.functions.this_thread.current_info['user']['location']['latitude']
- self.longitude = docassemble.base.functions.this_thread.current_info['user']['location']['longitude']
+ if 'user' in this_thread.current_info and 'location' in this_thread.current_info['user'] and isinstance(this_thread.current_info['user']['location'], dict):
+ if 'latitude' in this_thread.current_info['user']['location'] and 'longitude' in this_thread.current_info['user']['location']:
+ self.latitude = this_thread.current_info['user']['location']['latitude']
+ self.longitude = this_thread.current_info['user']['location']['longitude']
self.known = True
# logmessage("known is true")
- elif 'error' in docassemble.base.functions.this_thread.current_info['user']['location']:
- self.error = docassemble.base.functions.this_thread.current_info['user']['location']['error']
+ elif 'error' in this_thread.current_info['user']['location']:
+ self.error = this_thread.current_info['user']['location']['error']
self.known = False
# logmessage("known is false")
self.gathered = True
@@ -10639,7 +10312,7 @@ def send_email(self, roles_needed, **kwargs):
bool: True if an email was successfully sent; False if no email
was necessary or sending failed.
"""
- # logmessage("Current role is " + str(docassemble.base.functions.this_thread.global_vars.role))
+ # logmessage("Current role is " + str(this_thread.global_vars.role))
for key, val in kwargs.items(): # pylint: disable=unused-variable
if 'to' in val:
need(val['to'].email)
@@ -11019,11 +10692,11 @@ def geocode(self, address=None, reset=False):
else:
the_address = address
# logmessage("geocode: trying to geocode " + str(the_address))
- geocoder_service = server.daconfig.get('geocoder service', 'google maps')
+ geocoder_service = get_configuration().get('geocoder service', 'google maps')
if geocoder_service == 'google maps':
- geocoder = docassemble.base.geocode.GoogleV3GeoCoder(server=server)
+ geocoder = GoogleV3GeoCoder()
elif geocoder_service == 'azure maps':
- geocoder = docassemble.base.geocode.AzureMapsGeoCoder(server=server)
+ geocoder = AzureMapsGeoCoder()
else:
self._geocoded = True
self.geolocated = True
@@ -11151,7 +10824,7 @@ def block(self, language=None, international=False, show_country=None):
str: Multi-line address block (lines joined with ``[NEWLINE]``
markers in non-DOCX contexts).
"""
- if docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if this_thread.evaluation_context == 'docx':
line_breaker = ''
else:
line_breaker = " [NEWLINE] "
@@ -11421,7 +11094,7 @@ def _map_info(self):
result = {'latitude': self.location.latitude, 'longitude': self.location.longitude, 'info': the_info}
if hasattr(self, 'icon'):
result['icon'] = self.icon
- elif self is docassemble.base.functions.this_thread.global_vars.user:
+ elif self is this_thread.global_vars.user:
result['icon'] = DEFAULT_BLUE_ICON
return [result]
return None
@@ -11458,15 +11131,15 @@ def pronoun_objective(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- output = docassemble.base.functions.you_objective(**kwargs)
+ output = you_objective(**kwargs)
elif person == '2p':
- output = docassemble.base.functions.you_objective_plural(**kwargs)
+ output = you_objective_plural(**kwargs)
elif person == '1':
- output = docassemble.base.functions.me_objective(**kwargs)
+ output = me_objective(**kwargs)
elif person == '1p':
- output = docassemble.base.functions.us_objective(**kwargs)
+ output = us_objective(**kwargs)
else:
- output = docassemble.base.functions.it_objective(**kwargs)
+ output = it_objective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize(output)
return output
@@ -11486,11 +11159,11 @@ def object_possessive(self, target, **kwargs):
if person == '2':
return your(target, **kwargs)
if person == '2p':
- return docassemble.base.functions.your_plural(target, **kwargs)
+ return your_plural(target, **kwargs)
if person == '1':
- return docassemble.base.functions.my_possessive(target, **kwargs)
+ return my_possessive(target, **kwargs)
if person == '1p':
- return docassemble.base.functions.our_possessive(target, **kwargs)
+ return our_possessive(target, **kwargs)
return super().object_possessive(target, **kwargs)
def is_are_you(self, **kwargs):
@@ -11505,13 +11178,13 @@ def is_are_you(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- output = docassemble.base.functions.are_you(**kwargs)
+ output = are_you(**kwargs)
elif person == '2p':
- output = docassemble.base.functions.are_you_plural(**kwargs)
+ output = are_you_plural(**kwargs)
elif person == '1':
- output = docassemble.base.functions.am_i(**kwargs)
+ output = am_i(**kwargs)
elif person == '1p':
- output = docassemble.base.functions.are_we(**kwargs)
+ output = are_we(**kwargs)
else:
output = is_word(str(self), **kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
@@ -11529,7 +11202,7 @@ def address_block(self, language=None, international=False, show_country=False):
Returns:
str: Name followed by the address block.
"""
- if docassemble.base.functions.this_thread.evaluation_context == 'docx':
+ if this_thread.evaluation_context == 'docx':
return self.name.full() + ' ' + self.address.block(language=language, international=international, show_country=show_country)
return "[FLUSHLEFT] " + self.name.full() + " [NEWLINE] " + self.address.block(language=language, international=international, show_country=show_country)
@@ -11728,8 +11401,8 @@ def first_name_hint(self):
current user and they are authenticated; otherwise an empty
string.
"""
- if self is docassemble.base.functions.this_thread.global_vars.user and docassemble.base.functions.this_thread.current_info['user']['is_authenticated'] and 'firstname' in docassemble.base.functions.this_thread.current_info['user'] and docassemble.base.functions.this_thread.current_info['user']['firstname']:
- return docassemble.base.functions.this_thread.current_info['user']['firstname']
+ if self is this_thread.global_vars.user and this_thread.current_info['user']['is_authenticated'] and 'firstname' in this_thread.current_info['user'] and this_thread.current_info['user']['firstname']:
+ return this_thread.current_info['user']['firstname']
return ''
def last_name_hint(self):
@@ -11740,8 +11413,8 @@ def last_name_hint(self):
current user and they are authenticated; otherwise an empty
string.
"""
- if self is docassemble.base.functions.this_thread.global_vars.user and docassemble.base.functions.this_thread.current_info['user']['is_authenticated'] and 'lastname' in docassemble.base.functions.this_thread.current_info['user'] and docassemble.base.functions.this_thread.current_info['user']['lastname']:
- return docassemble.base.functions.this_thread.current_info['user']['lastname']
+ if self is this_thread.global_vars.user and this_thread.current_info['user']['is_authenticated'] and 'lastname' in this_thread.current_info['user'] and this_thread.current_info['user']['lastname']:
+ return this_thread.current_info['user']['lastname']
return ''
def salutation(self, **kwargs):
@@ -11776,11 +11449,11 @@ def pronoun_possessive(self, target, **kwargs):
if person == '2':
output = your(target, **kwargs)
elif person == '2p':
- output = docassemble.base.functions.your_plural(target, **kwargs)
+ output = your_plural(target, **kwargs)
elif person == '1':
- output = docassemble.base.functions.my_possessive(target, **kwargs)
+ output = my_possessive(target, **kwargs)
elif person == '1p':
- output = docassemble.base.functions.our_possessive(target, **kwargs)
+ output = our_possessive(target, **kwargs)
elif self.gender == 'female':
output = her(target, **kwargs)
elif self.gender == 'other':
@@ -11803,19 +11476,19 @@ def pronoun(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- output = docassemble.base.functions.you_objective(**kwargs)
+ output = you_objective(**kwargs)
elif person == '2p':
- output = docassemble.base.functions.you_objective_plural(**kwargs)
+ output = you_objective_plural(**kwargs)
elif person == '1':
- output = docassemble.base.functions.me_objective(**kwargs)
+ output = me_objective(**kwargs)
elif person == '1p':
- output = docassemble.base.functions.our_objective(**kwargs)
+ output = our_objective(**kwargs)
elif self.gender == 'female':
- output = docassemble.base.functions.her_objective(**kwargs)
+ output = her_objective(**kwargs)
elif self.gender == 'other':
- output = docassemble.base.functions.genderless_objective(**kwargs)
+ output = genderless_objective(**kwargs)
else:
- output = docassemble.base.functions.him_objective(**kwargs)
+ output = him_objective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize(output)
return output
@@ -11845,19 +11518,19 @@ def pronoun_subjective(self, **kwargs):
else:
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- output = docassemble.base.functions.you_subjective(**kwargs)
+ output = you_subjective(**kwargs)
elif person == '2p':
- output = docassemble.base.functions.you_subjective_plural(**kwargs)
+ output = you_subjective_plural(**kwargs)
elif person == '1':
- output = docassemble.base.functions.i_subjective(**kwargs)
+ output = i_subjective(**kwargs)
elif person == '1p':
- output = docassemble.base.functions.we_subjective(**kwargs)
+ output = we_subjective(**kwargs)
elif self.gender == 'female':
- output = docassemble.base.functions.she_subjective(**kwargs)
+ output = she_subjective(**kwargs)
elif self.gender == 'other':
- output = docassemble.base.functions.genderless_subjective(**kwargs)
+ output = genderless_subjective(**kwargs)
else:
- output = docassemble.base.functions.he_subjective(**kwargs)
+ output = he_subjective(**kwargs)
if 'capitalize' in kwargs and kwargs['capitalize']:
return capitalize(output)
return output
@@ -11875,18 +11548,18 @@ def itself(self, **kwargs):
"""
person = str(kwargs.pop('person', self.get_point_of_view()))
if person == '2':
- return docassemble.base.functions.yourself(**kwargs)
+ return yourself(**kwargs)
if person == '2p':
- return docassemble.base.functions.yourselves(**kwargs)
+ return yourselves(**kwargs)
if person == '1':
- return docassemble.base.functions.myself(**kwargs)
+ return myself(**kwargs)
if person == '1p':
- return docassemble.base.functions.ourselves(**kwargs)
+ return ourselves(**kwargs)
if self.gender == 'female':
- return docassemble.base.functions.herself(**kwargs)
+ return herself(**kwargs)
if self.gender == 'other':
- return docassemble.base.functions.genderless_self(**kwargs)
- return docassemble.base.functions.himself(**kwargs)
+ return genderless_self(**kwargs)
+ return himself(**kwargs)
def __setattr__(self, attrname, the_value):
if attrname == 'name' and isinstance(the_value, str):
@@ -12174,7 +11847,7 @@ def get_sms_session(phone_number, config='default'):
dict or None: Session data dict (without internal keys), or None if no
session exists for the number.
"""
- result = server.get_sms_session(phone_number, config=config)
+ result = server_get_sms_session(phone_number, config=config)
for key in ['number', 'tempuser', 'user_id']:
if key in result:
del result[key]
@@ -12196,7 +11869,7 @@ def initiate_sms_session(phone_number, yaml_filename=None, email=None, new=False
Returns:
bool: True.
"""
- server.initiate_sms_session(phone_number, yaml_filename=yaml_filename, email=email, new=new, config=config)
+ server_initiate_sms_session(phone_number, yaml_filename=yaml_filename, email=email, new=new, config=config)
if send:
send_sms_invite(to=phone_number, config=config)
return True
@@ -12212,7 +11885,7 @@ def terminate_sms_session(phone_number, config='default'):
Returns:
bool: True if a session was terminated; False otherwise.
"""
- return server.terminate_sms_session(phone_number, config=config)
+ return server_terminate_sms_session(phone_number, config=config)
def send_sms_invite(to=None, body='question', config='default'):
@@ -12222,10 +11895,10 @@ def send_sms_invite(to=None, body='question', config='default'):
"""
if to is None:
raise DAError("send_sms_invite: no phone number provided")
- phone_number = docassemble.base.functions.phone_number_in_e164(to)
+ phone_number = phone_number_in_e164(to)
if phone_number is None:
raise DAError("send_sms_invite: phone number is invalid")
- the_message = server.sms_body(phone_number, body=body, config=config)
+ the_message = sms_body(phone_number, body=body, config=config)
# logmessage("Sending message " + str(message) + " to " + str(phone_number))
send_sms(to=phone_number, body=the_message, config=config)
@@ -12250,13 +11923,14 @@ def send_sms(to=None, body=None, template=None, task=None, task_persistent=False
Returns:
bool: True if the message was sent successfully; False otherwise.
"""
- if server.twilio_config is None:
+ twilio_config = get_twilio_config()
+ if twilio_config is None:
logmessage("send_sms: ignoring because Twilio not enabled")
return False
- if config not in server.twilio_config['name']:
+ if config not in twilio_config['name']:
logmessage("send_sms: ignoring because requested configuration does not exist")
return False
- tconfig = server.twilio_config['name'][config]
+ tconfig = twilio_config['name'][config]
if 'sms' not in tconfig or tconfig['sms'] in [False, None]:
logmessage("send_sms: ignoring because SMS not enabled")
return False
@@ -12311,7 +11985,7 @@ def send_sms(to=None, body=None, template=None, task=None, task_persistent=False
if success:
for the_attachment in attachment_list:
if isinstance(the_attachment, DAFile) and the_attachment.ok:
- # url = url_start + server.url_for('serve_stored_file', uid=docassemble.base.functions.this_thread.current_info['session'], number=the_attachment.number, filename=the_attachment.filename, extension=the_attachment.extension)
+ # url = url_start + url_for('serve_stored_file', uid=this_thread.current_info['session'], number=the_attachment.number, filename=the_attachment.filename, extension=the_attachment.extension)
media.append(the_attachment.url_for(_external=True))
if isinstance(the_attachment, DAStaticFile):
media.append(the_attachment.url_for(_external=True))
@@ -12352,7 +12026,7 @@ def __init__(self, sid):
def status(self):
if self.sid is None:
return 'not-configured'
- the_json = server.server_redis.get('da:faxcallback:sid:' + self.sid)
+ the_json = get_server_redis().get('da:faxcallback:sid:' + self.sid)
if the_json is None:
return 'no-information'
info = json.loads(the_json)
@@ -12367,7 +12041,7 @@ def status(self):
def pages(self):
if self.sid is None:
return 0
- the_json = server.server_redis.get('da:faxcallback:sid:' + self.sid)
+ the_json = get_server_redis().get('da:faxcallback:sid:' + self.sid)
if the_json is None:
return 0
info = json.loads(the_json)
@@ -12382,7 +12056,7 @@ def pages(self):
def info(self):
if self.sid is None:
return {'FaxStatus': 'not-configured'}
- the_json = server.server_redis.get('da:faxcallback:sid:' + self.sid)
+ the_json = get_server_redis().get('da:faxcallback:sid:' + self.sid)
if the_json is None:
return {'FaxStatus': 'no-information'}
info_dict = json.loads(the_json)
@@ -12422,7 +12096,7 @@ def send_fax(fax_number, file_object, config='default', country=None):
file_object = file_object.elements[0]
else:
file_object = pdf_concatenate(file_object)
- return FaxStatus(server.send_fax(fax_string(fax_number, country=country), file_object, config, country=country))
+ return FaxStatus(server_send_fax(fax_string(fax_number, country=country), file_object, config, country=country))
def send_email(to=None, sender=None, reply_to=None, cc=None, bcc=None, body=None, html=None, subject="", template=None, task=None, task_persistent=False, attachments=None, mailgun_variables=None, dry_run=False, config=None):
@@ -12462,7 +12136,7 @@ def send_email(to=None, sender=None, reply_to=None, cc=None, bcc=None, body=None
False otherwise.
"""
if config is None:
- config = docassemble.base.functions.this_thread.interview.consolidated_metadata.get('email config', None)
+ config = this_thread.interview.consolidated_metadata.get('email config', None)
if not config:
config = 'default'
if attachments is None:
@@ -12492,6 +12166,7 @@ def send_email(to=None, sender=None, reply_to=None, cc=None, bcc=None, body=None
cc_string = email_stringer(cc, include_name=None)
bcc_string = email_stringer(bcc, include_name=None)
# logmessage("Sending mail to: " + repr({'subject': subject, 'recipients': to_string, 'sender': sender_string, 'cc': cc_string, 'bcc': bcc_string, 'body': body, 'html': html}))
+ Message = get_mail_class() # pylint: disable=invalid-name
msg = Message(subject, sender=sender_string, reply_to=reply_to_string, recipients=to_string, cc=cc_string, bcc=bcc_string, body=body, html=html)
if mailgun_variables is not None:
if isinstance(mailgun_variables, dict):
@@ -12523,7 +12198,7 @@ def send_email(to=None, sender=None, reply_to=None, cc=None, bcc=None, body=None
elif isinstance(attachment, DAFileList):
attachment_list.extend(attachment.elements)
elif isinstance(attachment, str):
- file_info = server.file_finder(attachment)
+ file_info = file_finder(attachment)
if 'fullpath' in file_info and file_info['fullpath'] is not None:
failed = True
with open(file_info['fullpath'], 'rb') as fp:
@@ -12542,14 +12217,14 @@ def send_email(to=None, sender=None, reply_to=None, cc=None, bcc=None, body=None
the_path = the_attachment.path()
with open(the_path, 'rb') as fp:
the_basename = os.path.basename(the_path)
- extension, mimetype = server.get_ext_and_mimetype(the_basename) # pylint: disable=assignment-from-none,unpacking-non-sequence,unused-variable
+ extension, mimetype = get_ext_and_mimetype(the_basename) # pylint: disable=assignment-from-none,unpacking-non-sequence,unused-variable
msg.attach(attachment_name(the_basename, filenames_used), mimetype, fp.read())
continue
if the_attachment.ok:
if the_attachment.has_specific_filename:
- file_info = server.file_finder(str(the_attachment.number), filename=the_attachment.filename)
+ file_info = file_finder(str(the_attachment.number), filename=the_attachment.filename)
else:
- file_info = server.file_finder(str(the_attachment.number))
+ file_info = file_finder(str(the_attachment.number))
if 'fullpath' in file_info and file_info['fullpath'] is not None:
failed = True
with open(file_info['fullpath'], 'rb') as fp:
@@ -12564,7 +12239,7 @@ def send_email(to=None, sender=None, reply_to=None, cc=None, bcc=None, body=None
if success:
try:
logmessage("send_email: starting to send")
- server.send_mail(msg, config=config)
+ send_mail(msg, config=config)
logmessage("send_email: finished sending")
except BaseException as errmess:
logmessage("send_email: sending mail failed with error of " + " type " + str(errmess.__class__.__name__) + ": " + str(errmess))
@@ -12615,7 +12290,7 @@ def map_of(*pargs, **kwargs):
if markers:
for marker in markers:
if 'icon' in marker and not isinstance(marker['icon'], dict):
- marker['icon'] = {'url': server.url_finder(marker['icon'])}
+ marker['icon'] = {'url': url_finder(marker['icon'])}
if 'info' in marker and marker['info']:
marker['info'] = markdown_to_html(marker['info'], trim=True, external=True)
the_map['markers'].append(marker)
@@ -12663,7 +12338,7 @@ def ocr_file_in_background(*pargs, **kwargs):
else:
ui_notification = None
if kwargs.get('use_google', False):
- the_task = server.ocr_google_in_background(image_file, kwargs.get('raw_result', False), docassemble.base.functions.this_thread.current_info['session']) # pylint: disable=assignment-from-none
+ the_task = ocr_google_in_background(image_file, kwargs.get('raw_result', False), this_thread.current_info['session']) # pylint: disable=assignment-from-none
else:
language = kwargs.get('language', None)
arg_f = int_or_none(kwargs.get('f', None))
@@ -12671,21 +12346,22 @@ def ocr_file_in_background(*pargs, **kwargs):
arg_psm = kwargs.get('psm', 6)
arg_x = int_or_none(kwargs.get('x', None))
arg_y = int_or_none(kwargs.get('y', None))
- arg_W = int_or_none(kwargs.get('W', None))
- arg_H = int_or_none(kwargs.get('H', None))
+ arg_W = int_or_none(kwargs.get('W', None)) # pylint: disable=invalid-name
+ arg_H = int_or_none(kwargs.get('H', None)) # pylint: disable=invalid-name
the_message = kwargs.get('message', None)
- args = {'yaml_filename': docassemble.base.functions.this_thread.current_info['yaml_filename'], 'user': docassemble.base.functions.this_thread.current_info['user'], 'user_code': docassemble.base.functions.this_thread.current_info['session'], 'secret': docassemble.base.functions.this_thread.current_info['secret'], 'url': docassemble.base.functions.this_thread.current_info['url'], 'url_root': docassemble.base.functions.this_thread.current_info['url_root'], 'language': language, 'f': arg_f, 'l': arg_l, 'psm': arg_psm, 'x': arg_x, 'y': arg_y, 'W': arg_W, 'H': arg_H, 'extra': ui_notification, 'message': the_message, 'pdf': False, 'preserve_color': False}
- collector = server.ocr_finalize.s(**args)
+ args = {'yaml_filename': this_thread.current_info['yaml_filename'], 'user': this_thread.current_info['user'], 'user_code': this_thread.current_info['session'], 'secret': this_thread.current_info['secret'], 'url': this_thread.current_info['url'], 'url_root': this_thread.current_info['url_root'], 'language': language, 'f': arg_f, 'l': arg_l, 'psm': arg_psm, 'x': arg_x, 'y': arg_y, 'W': arg_W, 'H': arg_H, 'extra': ui_notification, 'message': the_message, 'pdf': False, 'preserve_color': False}
+ celery_app = get_celery_app()
+ collector = celery_app.signature('tasks.ocr_finalize', kwargs=args)
todo = []
indexno = 0
for item in ocr_page_tasks(image_file, **args):
- todo.append(server.ocr_page.s(indexno, **item))
+ todo.append(celery_app.signature('tasks.ocr_page', args=[indexno], kwargs=item))
indexno += 1
- the_task = server.chord(todo)(collector) # pylint: disable=assignment-from-none
+ the_task = chord(todo)(collector) # pylint: disable=assignment-from-none
if ui_notification is not None:
- worker_key = 'da:worker:uid:' + str(docassemble.base.functions.this_thread.current_info['session']) + ':i:' + str(docassemble.base.functions.this_thread.current_info['yaml_filename']) + ':userid:' + str(docassemble.base.functions.this_thread.current_info['user']['the_user_id'])
+ worker_key = 'da:worker:uid:' + str(this_thread.current_info['session']) + ':i:' + str(this_thread.current_info['yaml_filename']) + ':userid:' + str(this_thread.current_info['user']['the_user_id'])
# logmessage("worker_caller: id is " + str(result.obj.id) + " and key is " + worker_key)
- server.server_redis.rpush(worker_key, the_task.id)
+ get_server_redis().rpush(worker_key, the_task.id)
# logmessage("ocr_file_in_background finished")
return the_task
@@ -12693,11 +12369,11 @@ def ocr_file_in_background(*pargs, **kwargs):
# """Starts optical character recognition on one or more image files or PDF
# files and returns an object representing the background task created."""
# logmessage("ocr_file_in_background: started")
-# return server.async_ocr(image_file, ui_notification=ui_notification, language=language, psm=psm, x=x, y=y, W=W, H=H, user_code=docassemble.base.functions.this_thread.current_info.get('session', None))
+# return server.async_ocr(image_file, ui_notification=ui_notification, language=language, psm=psm, x=x, y=y, W=W, H=H, user_code=this_thread.current_info.get('session', None))
def get_work_bucket():
- bucket_name = server.daconfig['google'].get('work bucket', None)
+ bucket_name = get_configuration()['google'].get('work bucket', None)
if bucket_name is None:
raise DAError("Cannot use Google Storage unless there is a work bucket configured in the google configuration")
api = DAGoogleAPI()
@@ -12715,7 +12391,7 @@ def get_work_bucket():
def google_ocr_file(image_file, raw_result=False):
if isinstance(image_file, DAFile):
image_file = [image_file]
- api = docassemble.base.util.DAGoogleAPI()
+ api = DAGoogleAPI()
client = api.google_cloud_vision_client()
if raw_result:
output = []
@@ -12792,7 +12468,7 @@ def google_ocr_file(image_file, raw_result=False):
return output
-def ocr_file(image_file, language=None, psm=6, f=None, l=None, x=None, y=None, W=None, H=None, use_google=False, raw_result=False): # noqa: E741
+def ocr_file(image_file, language=None, psm=6, f=None, l=None, x=None, y=None, W=None, H=None, use_google=False, raw_result=False): # noqa: E741 # pylint: disable=invalid-name
"""Run optical character recognition on image or PDF files and return the text.
Args:
@@ -12888,6 +12564,7 @@ def ocr_file(image_file, language=None, psm=6, f=None, l=None, x=None, y=None, W
except subprocess.CalledProcessError as err:
raise DAError("ocr_file: failed to OCR file: " + str(err) + " " + str(err.output.decode()))
elif TESSERACT_MODE == REMOTE:
+ from docassemble.tesseract.tasks import run_tesseract # pylint: disable=import-error,no-name-in-module,ungrouped-imports
result = run_tesseract.delay(['stdin', 'stdout', '-l', str(lang), '--psm', str(psm)], mode=0, file_path=file_to_read.name).get(disable_sync_subtasks=False) # pylint: disable=possibly-used-before-assignment
if not result.ok:
raise DAError("ocr_file: failed to OCR file")
@@ -12900,7 +12577,7 @@ def ocr_file(image_file, language=None, psm=6, f=None, l=None, x=None, y=None, W
return "\f".join(page_text)
-def read_qr(image_file, f=None, l=None, x=None, y=None, W=None, H=None): # noqa: E741
+def read_qr(image_file, f=None, l=None, x=None, y=None, W=None, H=None): # noqa: E741 # pylint: disable=invalid-name
"""Decode QR codes found in image or PDF files.
Args:
@@ -12989,7 +12666,7 @@ def path_and_mimetype(file_ref):
file_ref = file_ref._first_file()
elif isinstance(file_ref, DAStaticFile):
path = file_ref.path()
- extension, mimetype = server.get_ext_and_mimetype(file_ref.filename) # pylint: disable=unpacking-non-sequence,unused-variable,assignment-from-none
+ extension, mimetype = get_ext_and_mimetype(file_ref.filename) # pylint: disable=unpacking-non-sequence,unused-variable,assignment-from-none
return path, mimetype
if isinstance(file_ref, DAFile):
if hasattr(file_ref, 'mimetype'):
@@ -12997,7 +12674,7 @@ def path_and_mimetype(file_ref):
else:
mime_type = None
return file_ref.path(), mime_type
- file_info = server.file_finder(file_ref, return_nonexistent=True)
+ file_info = file_finder(file_ref, return_nonexistent=True)
return file_info.get('fullpath', None), file_info.get('mimetype', None)
@@ -13184,7 +12861,7 @@ def pdf_concatenate(*pargs, **kwargs):
if len(paths) == 0:
raise DAError("pdf_concatenate: no valid files to concatenate")
(owner_password, password) = get_passwords(kwargs.get('password', None))
- pdf_path = docassemble.base.pandoc.concatenate_files(paths, pdfa=kwargs.get('pdfa', False), password=password, owner_password=owner_password)
+ pdf_path = concatenate_files(paths, pdfa=kwargs.get('pdfa', False), password=password, owner_password=owner_password)
pdf_file = kwargs.get('output_to', None)
if pdf_file is None:
pdf_file = DAFile()
@@ -13232,7 +12909,7 @@ def recurse_zip_params(param, root, files):
elif isinstance(param, (DAStaticFile, DAFile)):
files.append((root + param.filename, param.path()))
else:
- file_info = server.file_finder(param)
+ file_info = file_finder(param)
files.append((root + file_info['filename'], file_info['fullpath']))
return files
@@ -13483,7 +13160,7 @@ def action_button_html(url, icon=None, color='success', size='sm', block=False,
id_tag = ''
else:
id_tag = ' id=' + json.dumps(id_tag)
- return '' + icon + word(label) + ' '
+ return '' + icon + word(label) + ' '
def overlay_pdf(main_pdf, logo_pdf, first_page=None, last_page=None, logo_page=None, only=None, multi=False, output_to=None, filename=None):
@@ -13539,9 +13216,9 @@ def overlay_pdf(main_pdf, logo_pdf, first_page=None, last_page=None, logo_page=N
filename = 'file.pdf'
outfile.initialize(extension='pdf', filename=filename, reinitialize=outfile.ok)
if multi:
- docassemble.base.pdftk.overlay_pdf_multi(main_file, logo_file, outfile.path())
+ overlay_pdf_multi(main_file, logo_file, outfile.path())
else:
- docassemble.base.pdftk.overlay_pdf(main_file, logo_file, outfile.path(), first_page=first_page, last_page=last_page, logo_page=logo_page, only=only)
+ pdftk_overlay_pdf(main_file, logo_file, outfile.path(), first_page=first_page, last_page=last_page, logo_page=logo_page, only=only)
outfile.commit()
outfile.retrieve()
return outfile
@@ -13555,12 +13232,12 @@ def explain(the_explanation, category='default'):
category (str): Category name for grouping explanations. Defaults to
``'default'``.
"""
- if 'explanations' not in docassemble.base.functions.this_thread.internal:
- docassemble.base.functions.this_thread.internal['explanations'] = {}
- if category not in docassemble.base.functions.this_thread.internal['explanations']:
- docassemble.base.functions.this_thread.internal['explanations'][category] = []
- if the_explanation not in docassemble.base.functions.this_thread.internal['explanations'][category]:
- docassemble.base.functions.this_thread.internal['explanations'][category].append(the_explanation)
+ if 'explanations' not in this_thread.internal:
+ this_thread.internal['explanations'] = {}
+ if category not in this_thread.internal['explanations']:
+ this_thread.internal['explanations'][category] = []
+ if the_explanation not in this_thread.internal['explanations'][category]:
+ this_thread.internal['explanations'][category].append(the_explanation)
def clear_explanations(category='default'):
@@ -13570,13 +13247,13 @@ def clear_explanations(category='default'):
category (str): Category to clear, or ``'all'`` to clear every
category. Defaults to ``'default'``.
"""
- if 'explanations' not in docassemble.base.functions.this_thread.internal:
+ if 'explanations' not in this_thread.internal:
return
if category == 'all':
- docassemble.base.functions.this_thread.internal['explanations'] = {}
- if category not in docassemble.base.functions.this_thread.internal['explanations']:
+ this_thread.internal['explanations'] = {}
+ if category not in this_thread.internal['explanations']:
return
- docassemble.base.functions.this_thread.internal['explanations'][category] = []
+ this_thread.internal['explanations'][category] = []
def logic_explanation(category='default'):
@@ -13588,9 +13265,9 @@ def logic_explanation(category='default'):
Returns:
list[str]: Explanation strings recorded for the category, in order.
"""
- if 'explanations' not in docassemble.base.functions.this_thread.internal:
+ if 'explanations' not in this_thread.internal:
return []
- return docassemble.base.functions.this_thread.internal['explanations'].get(category, [])
+ return this_thread.internal['explanations'].get(category, [])
def set_status(**kwargs):
@@ -13600,10 +13277,10 @@ def set_status(**kwargs):
**kwargs: Arbitrary key-value pairs to store in the session's internal
``'misc'`` dictionary.
"""
- if 'misc' not in docassemble.base.functions.this_thread.internal:
- docassemble.base.functions.this_thread.internal['misc'] = {}
+ if 'misc' not in this_thread.internal:
+ this_thread.internal['misc'] = {}
for key, val in kwargs.items():
- docassemble.base.functions.this_thread.internal['misc'][key] = val
+ this_thread.internal['misc'][key] = val
def get_status(setting):
@@ -13615,9 +13292,9 @@ def get_status(setting):
Returns:
object or None: The stored value, or None if the key does not exist.
"""
- if 'misc' not in docassemble.base.functions.this_thread.internal:
+ if 'misc' not in this_thread.internal:
return None
- return docassemble.base.functions.this_thread.internal['misc'].get(setting, None)
+ return this_thread.internal['misc'].get(setting, None)
def prevent_dependency_satisfaction(f):
@@ -13669,31 +13346,31 @@ def assemble_docx(input_file, fields=None, output_path=None, output_format='docx
output_path = output_file.name
else:
using_temporary_file = False
- the_fields = copy.copy(docassemble.base.functions.get_user_dict())
+ the_fields = copy.copy(get_user_dict())
if isinstance(fields, dict):
the_fields.update(fields)
try:
docx_template = DocxTemplate(input_file)
docx_template.render_init()
- docassemble.base.functions.set_context('docx', template=docx_template)
- the_env = docassemble.base.parse.custom_jinja_env()
+ set_context('docx', template=docx_template)
+ the_env = custom_jinja_env()
the_xml = docx_template.get_xml()
the_xml = re.sub(r'])', r'\n old_count and old_count < 10:
+ old_count = this_thread.misc.get('docx_include_count', 0)
+ docx_template.render(the_fields, jinja_env=custom_jinja_env())
+ if this_thread.misc.get('docx_include_count', 0) > old_count and old_count < 10:
new_template_file = tempfile.NamedTemporaryFile(prefix="datemp", mode="wb", suffix=".docx", delete=False)
docx_template.save(new_template_file.name)
docx_template = DocxTemplate(new_template_file.name)
docx_template.render_init()
- docassemble.base.functions.this_thread.misc['docx_template'] = docx_template
+ this_thread.misc['docx_template'] = docx_template
else:
break
- subdocs = docassemble.base.functions.this_thread.misc.get('docx_subdocs', [])
+ subdocs = this_thread.misc.get('docx_subdocs', [])
the_template_docx = docx_template.docx
for subdoc in subdocs:
docassemble.base.file_docx.fix_subdoc(the_template_docx, subdoc)
@@ -13701,7 +13378,7 @@ def assemble_docx(input_file, fields=None, output_path=None, output_format='docx
if (not hasattr(the_error, 'filename')) or the_error.filename is None:
the_error.filename = os.path.basename(input_file)
raise the_error
- docassemble.base.functions.reset_context()
+ reset_context()
if output_format == 'docx':
docx_template.save(output_path)
docassemble.base.file_docx.fix_docx(output_path)
@@ -13711,7 +13388,7 @@ def assemble_docx(input_file, fields=None, output_path=None, output_format='docx
docassemble.base.file_docx.fix_docx(temp_file.name)
if not isinstance(pdf_options, dict):
pdf_options = {}
- result = docassemble.base.pandoc.word_to_pdf(temp_file.name, 'docx', output_path, pdfa=pdf_options.get('pdfa', False), password=pdf_options.get('password', None), owner_password=pdf_options.get('owner_password', None), update_refs=pdf_options.get('update_refs', False), tagged=pdf_options.get('tagged', False), filename=filename)
+ result = word_to_pdf(temp_file.name, 'docx', output_path, pdfa=pdf_options.get('pdfa', False), password=pdf_options.get('password', None), owner_password=pdf_options.get('owner_password', None), update_refs=pdf_options.get('update_refs', False), tagged=pdf_options.get('tagged', False), filename=filename)
if not result:
raise DAError("Error converting to PDF")
elif output_format == 'md':
@@ -13719,7 +13396,7 @@ def assemble_docx(input_file, fields=None, output_path=None, output_format='docx
docx_template.save(temp_file.name)
docassemble.base.file_docx.fix_docx(temp_file.name)
if can_convert_word_to_markdown():
- result = docassemble.base.pandoc.word_to_markdown(temp_file.name, 'docx')
+ result = word_to_markdown(temp_file.name, 'docx')
else:
result = None
if not result:
@@ -13761,15 +13438,7 @@ def omg_filter(text):
register_jinja_filter('omg', omg_filter)
"""
- return docassemble.base.parse.register_jinja_filter(filter_name, func)
-
-
-def variables_snapshot_connection():
- return server.variables_snapshot_connection()
-
-
-def variables_snapshot_connect():
- return server.variables_snapshot_connect()
+ return jinja_register_jinja_filter(filter_name, func)
def get_persistent_task_store(persistent):
@@ -13778,7 +13447,7 @@ def get_persistent_task_store(persistent):
else:
base = persistent
if base == 'session':
- encrypted = docassemble.base.functions.this_thread.current_info.get('encrypted', True)
+ encrypted = this_thread.current_info.get('encrypted', True)
store = DAStore('store', base=base, encrypted=encrypted)
else:
store = DAStore('store', base=base)
@@ -13805,7 +13474,7 @@ def task_performed(task, persistent=False):
if task in tasks and tasks[task]: # pylint: disable=unsubscriptable-object,unsupported-membership-test
return True
return False
- if task in docassemble.base.functions.this_thread.internal['tasks'] and docassemble.base.functions.this_thread.internal['tasks'][task]:
+ if task in this_thread.internal['tasks'] and this_thread.internal['tasks'][task]:
return True
return False
@@ -13847,10 +13516,10 @@ def mark_task_as_performed(task, persistent=False):
tasks[task] += 1 # pylint: disable=unsupported-assignment-operation
store.set('tasks', tasks)
return tasks[task] # pylint: disable=unsubscriptable-object
- if task not in docassemble.base.functions.this_thread.internal['tasks']:
- docassemble.base.functions.this_thread.internal['tasks'][task] = 0
- docassemble.base.functions.this_thread.internal['tasks'][task] += 1
- return docassemble.base.functions.this_thread.internal['tasks'][task]
+ if task not in this_thread.internal['tasks']:
+ this_thread.internal['tasks'][task] = 0
+ this_thread.internal['tasks'][task] += 1
+ return this_thread.internal['tasks'][task]
def times_task_performed(task, persistent=False):
@@ -13871,9 +13540,9 @@ def times_task_performed(task, persistent=False):
if task not in tasks: # pylint: disable=unsupported-membership-test
return 0
return tasks[task] # pylint: disable=unsubscriptable-object
- if task not in docassemble.base.functions.this_thread.internal['tasks']:
+ if task not in this_thread.internal['tasks']:
return 0
- return docassemble.base.functions.this_thread.internal['tasks'][task]
+ return this_thread.internal['tasks'][task]
def set_task_counter(task, times, persistent=False):
@@ -13892,7 +13561,7 @@ def set_task_counter(task, times, persistent=False):
tasks[task] = times # pylint: disable=unsupported-assignment-operation
store.set('tasks', tasks)
return
- docassemble.base.functions.this_thread.internal['tasks'][task] = times
+ this_thread.internal['tasks'][task] = times
def stash_data(data, expire=None):
@@ -13916,7 +13585,7 @@ def stash_data(data, expire=None):
assert expire > 0
except:
raise DAError("Invalid expire value")
- return server.stash_data(data, expire)
+ return server_stash_data(data, expire)
def retrieve_stashed_data(stash_key, secret, delete=False, refresh=False):
@@ -13935,7 +13604,7 @@ def retrieve_stashed_data(stash_key, secret, delete=False, refresh=False):
"""
if refresh and not (isinstance(refresh, int) and refresh > 0):
refresh = 60*60*24*90
- return server.retrieve_stashed_data(stash_key, secret, delete=delete, refresh=refresh)
+ return server_retrieve_stashed_data(stash_key, secret, delete=delete, refresh=refresh)
class DABreadCrumbs(DAObject):
@@ -13948,7 +13617,7 @@ def get_crumbs(self):
list[dict]: List of dicts with ``'breadcrumb'`` keys, representing
parent questions followed by the current question.
"""
- return docassemble.base.functions.get_action_stack()
+ return get_action_stack()
def show(self):
"""Return HTML for the breadcrumb navigation element.
@@ -14332,6 +14001,7 @@ def get_available_languages():
except subprocess.CalledProcessError as err:
raise DAError("get_available_languages: failed to list available languages: " + str(err))
elif TESSERACT_MODE == REMOTE:
+ from docassemble.tesseract.tasks import run_tesseract # pylint: disable=import-error,no-name-in-module,ungrouped-imports
result = run_tesseract.delay(['--list-langs'], mode=1).get(disable_sync_subtasks=False)
if not result.ok:
raise DAError("get_available_languages: failed to list available languages")
@@ -14343,7 +14013,7 @@ def get_available_languages():
return result
-def ocr_page_tasks(image_file, language=None, psm=6, f=None, l=None, x=None, y=None, W=None, H=None, user_code=None, user=None, pdf=False, preserve_color=False, **kwargs): # noqa: E741 # pylint: disable=unused-argument
+def ocr_page_tasks(image_file, language=None, psm=6, f=None, l=None, x=None, y=None, W=None, H=None, user_code=None, user=None, pdf=False, preserve_color=False, **kwargs): # noqa: E741 # pylint: disable=unused-argument, invalid-name
# logmessage("ocr_page_tasks running")
if isinstance(image_file, set):
return []
@@ -14508,6 +14178,7 @@ def ocr_pdf(*pargs, target=None, filename=None, lang=None, psm=6, dafilelist=Non
result = 1
logmessage("ocr_pdf: call to gs took too long")
elif TESSERACT_MODE == REMOTE:
+ from docassemble.tesseract.tasks import run_gs # pylint: disable=import-error,no-name-in-module,ungrouped-imports
result = run_gs.delay(params[1:]).get(disable_sync_subtasks=False) # pylint: disable=possibly-used-before-assignment
if result is None:
result = 1
@@ -14524,6 +14195,7 @@ def ocr_pdf(*pargs, target=None, filename=None, lang=None, psm=6, dafilelist=Non
result = 1
logmessage("ocr_pdf: call to tesseract took too long")
elif TESSERACT_MODE == REMOTE:
+ from docassemble.tesseract.tasks import run_tesseract # pylint: disable=import-error,no-name-in-module,ungrouped-imports
result = run_tesseract.delay(params[1:], mode=2).get(disable_sync_subtasks=False)
if result.ok:
result = result.content
@@ -14543,6 +14215,7 @@ def ocr_pdf(*pargs, target=None, filename=None, lang=None, psm=6, dafilelist=Non
result = 1
logmessage("ocr_pdf: call to tesseract took too long")
elif TESSERACT_MODE == REMOTE:
+ from docassemble.tesseract.tasks import run_tesseract # pylint: disable=import-error,no-name-in-module,ungrouped-imports
result = run_tesseract.delay(params[1:], mode=2).get(disable_sync_subtasks=False)
if result.ok:
result = result.content
@@ -14562,7 +14235,7 @@ def ocr_pdf(*pargs, target=None, filename=None, lang=None, psm=6, dafilelist=Non
shutil.copyfile(output[0], the_file.name)
source_file = the_file.name
else:
- source_file = docassemble.base.pandoc.concatenate_files(output)
+ source_file = concatenate_files(output)
if filename is None:
filename = 'file.pdf'
target.initialize(filename=filename, extension='pdf', mimetype='application/pdf', reinitialize=True)
@@ -14574,7 +14247,7 @@ def ocr_pdf(*pargs, target=None, filename=None, lang=None, psm=6, dafilelist=Non
return target
-def ocr_page(indexno, doc=None, lang=None, pdf_to_ppm='pdf_to_ppm', ocr_resolution=300, psm=6, page=None, x=None, y=None, W=None, H=None, user_code=None, user=None, pdf=False, preserve_color=False): # pylint: disable=unused-argument
+def ocr_page(indexno, doc=None, lang=None, pdf_to_ppm='pdf_to_ppm', ocr_resolution=300, psm=6, page=None, x=None, y=None, W=None, H=None, user_code=None, user=None, pdf=False, preserve_color=False): # pylint: disable=unused-argument, invalid-name
"""Runs optical character recognition on an image or a page of a PDF file and returns the recognized text."""
text = ''
if page is None:
@@ -14639,6 +14312,7 @@ def ocr_page(indexno, doc=None, lang=None, pdf_to_ppm='pdf_to_ppm', ocr_resoluti
except subprocess.CalledProcessError as err:
raise DAError("ocr_page: failed to run tesseract with command " + " ".join(params) + ": " + str(err) + " " + str(err.output.decode()))
elif TESSERACT_MODE == REMOTE:
+ from docassemble.tesseract.tasks import run_tesseract # pylint: disable=import-error,no-name-in-module,ungrouped-imports
result = run_tesseract.delay(params[1:], mode=0, file_path=file_to_read.name).get(disable_sync_subtasks=False)
if result.ok:
text = result.content
@@ -14657,6 +14331,7 @@ def ocr_page(indexno, doc=None, lang=None, pdf_to_ppm='pdf_to_ppm', ocr_resoluti
except subprocess.CalledProcessError as err:
raise DAError("ocr_page: failed to run tesseract with command " + " ".join(params) + ": " + str(err) + " " + str(err.output.decode()))
elif TESSERACT_MODE == REMOTE:
+ from docassemble.tesseract.tasks import run_tesseract # pylint: disable=import-error,no-name-in-module,ungrouped-imports
result = run_tesseract.delay(params[1:], mode=0, file_path=file_to_read.name).get(disable_sync_subtasks=False)
if result.ok:
text = result.content
@@ -14703,4 +14378,4 @@ def transform_json_variables(obj):
object: A JSON-serializable version of ``obj``, with docassemble types
converted to their plain Python equivalents.
"""
- return server.transform_json_variables(obj)
+ return transform_json_variables(obj)
diff --git a/docassemble_base/py.typed b/docassemble_base/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/docassemble_base/pyproject.toml b/docassemble_base/pyproject.toml
index c3eb8a979..33ec6f6b5 100644
--- a/docassemble_base/pyproject.toml
+++ b/docassemble_base/pyproject.toml
@@ -3,7 +3,7 @@ requires = ["setuptools==82.0.1"]
build-backend = "setuptools.build_meta"
[project]
name = "docassemble.base"
-version = '1.9.11'
+version = '1.10.1'
description = "The base components of the docassemble system."
readme = "README.md"
authors = [
@@ -36,14 +36,14 @@ dependencies = [
"cachetools==5.5.2",
"cairocffi==1.7.1",
"CairoSVG==2.8.2",
- "certifi==2025.11.12",
+ "certifi==2026.2.25",
"cffi==2.0.0",
"chardet==5.2.0",
"charset-normalizer==3.4.4",
"click==8.3.1",
"colorama==0.4.6",
"convertapi==2.0.0",
- "cryptography==46.0.7",
+ "cryptography==49.0.0",
"cssselect2==0.8.0",
"defusedxml==0.7.1",
"Deprecated==1.3.1",
@@ -81,7 +81,7 @@ dependencies = [
"httplib2==0.31.0",
"Hyphenate==1.1.0",
"id==1.5.0",
- "idna==3.11",
+ "idna==3.15",
"img2pdf==0.6.3",
"importlib_metadata==8.7.1",
"importlib_resources==6.5.2",
@@ -98,13 +98,13 @@ dependencies = [
"joblib==1.5.3",
"keyring==25.7.0",
"lxml==6.1.0",
- "Mako==1.3.11",
+ "Mako==1.3.12",
"Markdown==3.10",
"markdown-it-py==4.0.0",
"MarkupSafe==3.0.3",
"mdurl==0.1.2",
"more-itertools==11.0.1",
- "msal==1.34.0",
+ "msal==1.38.0rc1",
"msal-extensions==1.3.1",
"msgpack==1.1.2",
"msrest==0.7.1",
@@ -124,6 +124,7 @@ dependencies = [
"pikepdf==10.1.0",
"pillow==12.0.0",
"pkginfo==1.12.1.2",
+ "pluggy==1.6.0",
"portalocker==3.2.0",
"propcache==0.4.1",
"proto-plus==1.27.0",
@@ -136,7 +137,7 @@ dependencies = [
"pycryptodome==3.23.0",
"pycryptodomex==3.23.0",
"Pygments==2.20.0",
- "PyJWT==2.12.1",
+ "PyJWT==2.13.0",
"PyLaTeX==1.4.2",
"pyparsing==3.3.1",
"pypng==0.20220715.0",
@@ -202,3 +203,52 @@ Documentation = "https://docassemble.org/docs.html"
Repository = "https://github.com/jhpyle/docassemble.git"
"Bug Tracker" = "https://github.com/jhpyle/docassemble/issues"
Changelog = "https://github.com/jhpyle/docassemble/blob/master/CHANGELOG.md"
+[tool.ruff]
+src = ["docassemble_base", "docassemble_webapp", "docassemble_demo"]
+
+[tool.ruff.lint]
+ignore = ["E722", "E226", "E24", "E501", "C901", "E305", "E306"]
+
+[tool.pylint.main]
+init-hook = """
+import sys
+sys.path.insert(0, '../docassemble_webapp')
+sys.path.insert(0, '../docassemble_base')
+sys.path.insert(0, '../docassemble_demo')
+"""
+
+[tool.pylint.messages_control]
+disable = [
+ "attribute-defined-outside-init",
+ "bare-except",
+ "broad-exception-caught",
+ "consider-using-f-string",
+ "consider-using-with",
+ "eval-used",
+ "exec-used",
+ "import-outside-toplevel",
+ "line-too-long",
+ "missing-class-docstring",
+ "missing-function-docstring",
+ "missing-module-docstring",
+ "no-member",
+ "protected-access",
+ "raise-missing-from",
+ "too-few-public-methods",
+ "too-many-arguments",
+ "too-many-boolean-expressions",
+ "too-many-branches",
+ "too-many-function-args",
+ "too-many-instance-attributes",
+ "too-many-lines",
+ "too-many-locals",
+ "too-many-nested-blocks",
+ "too-many-positional-arguments",
+ "too-many-public-methods",
+ "too-many-return-statements",
+ "too-many-statements",
+ "global-statement",
+]
+
+[tool.mypy]
+disable_error_code = ["var-annotated"]
\ No newline at end of file
diff --git a/docassemble_demo/LICENSE.txt b/docassemble_demo/LICENSE.txt
index f9a98fdcf..a3c07394e 100644
--- a/docassemble_demo/LICENSE.txt
+++ b/docassemble_demo/LICENSE.txt
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2015-2025 Jonathan Pyle
+Copyright (c) 2015-2026 Jonathan Pyle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/docassemble_demo/docassemble/demo/__init__.py b/docassemble_demo/docassemble/demo/__init__.py
index 5928e6b57..dd7bc5697 100644
--- a/docassemble_demo/docassemble/demo/__init__.py
+++ b/docassemble_demo/docassemble/demo/__init__.py
@@ -1,2 +1,2 @@
# do not pre-load
-__version__ = "1.9.11"
+__version__ = "1.10.1"
diff --git a/docassemble_demo/docassemble/demo/aihelper.py b/docassemble_demo/docassemble/demo/aihelper.py
index df2293e1d..457a934b0 100644
--- a/docassemble_demo/docassemble/demo/aihelper.py
+++ b/docassemble_demo/docassemble/demo/aihelper.py
@@ -1,6 +1,6 @@
# do not pre-load
-from docassemble.base.util import get_config, DAObject, BackgroundAction, DADict
import anthropic
+from docassemble.base.util import get_config, DAObject, BackgroundAction, DADict
__all__ = ['AIHelper', 'AIInteraction']
diff --git a/docassemble_demo/docassemble/demo/alembic/env.py b/docassemble_demo/docassemble/demo/alembic/env.py
index 1dfc8c234..fddce29ae 100644
--- a/docassemble_demo/docassemble/demo/alembic/env.py
+++ b/docassemble_demo/docassemble/demo/alembic/env.py
@@ -20,7 +20,7 @@
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
-target_metadata = None
+target_metadata = None # pylint: disable=invalid-name
# other values from the config, defined by the needs of env.py,
# can be acquired:
diff --git a/docassemble_demo/docassemble/demo/calendar.py b/docassemble_demo/docassemble/demo/calendar.py
index 8f546c4dd..563008684 100644
--- a/docassemble_demo/docassemble/demo/calendar.py
+++ b/docassemble_demo/docassemble/demo/calendar.py
@@ -1,6 +1,6 @@
# do not pre-load
-from docassemble.base.util import as_datetime, DAFile
from ics import Calendar, Event, Attendee, Organizer
+from docassemble.base.util import as_datetime, DAFile
__all__ = ['make_event']
@@ -9,15 +9,15 @@ def make_event(title=None, location=None, description=None, begin_date=None, beg
if attendees is None:
attendees = []
if title is None:
- raise Exception("make_event: a title parameter is required")
+ raise RuntimeError("make_event: a title parameter is required")
if begin_date is None:
- raise Exception("make_event: a begin_date parameter is required")
+ raise RuntimeError("make_event: a begin_date parameter is required")
if begin_time is None:
- raise Exception("make_event: a begin_time parameter is required")
+ raise RuntimeError("make_event: a begin_time parameter is required")
if end_date is None:
- raise Exception("make_event: an end_date parameter is required")
+ raise RuntimeError("make_event: an end_date parameter is required")
if end_time is None:
- raise Exception("make_event: an end_time parameter is required")
+ raise RuntimeError("make_event: an end_time parameter is required")
c = Calendar()
e = Event()
if organizer is not None:
diff --git a/docassemble_demo/docassemble/demo/change_suffix.py b/docassemble_demo/docassemble/demo/change_suffix.py
index dad758bd5..59a6e4e59 100644
--- a/docassemble_demo/docassemble/demo/change_suffix.py
+++ b/docassemble_demo/docassemble/demo/change_suffix.py
@@ -1,8 +1,7 @@
# do not pre-load
-import docassemble.base.functions
-
+from docassemble.base.language.core import update_language_function
def my_name_suffix():
return ['Jr', 'Sr', 'II', 'III', 'IV', 'Esq', 'PhD']
-docassemble.base.functions.update_language_function('en', 'name_suffix', my_name_suffix)
+update_language_function('en', 'name_suffix', my_name_suffix)
diff --git a/docassemble_demo/docassemble/demo/chatbot_bg.py b/docassemble_demo/docassemble/demo/chatbot_bg.py
index ba20c143c..4c6722367 100644
--- a/docassemble_demo/docassemble/demo/chatbot_bg.py
+++ b/docassemble_demo/docassemble/demo/chatbot_bg.py
@@ -26,10 +26,10 @@ def ask(self, prompt):
if self.stage == 0:
self.bg_action = background_action(self.attr_name('bg_ask'), prompt=prompt)
self.stage = 1
- self.wait
+ self.wait # pylint: disable=pointless-statement
if self.stage == 1:
if not self.bg_action.ready():
- self.wait
+ self.wait # pylint: disable=pointless-statement
response = self.bg_action.get()
self.conversation.append({"role": "user", "content": prompt})
self.conversation.append({"role": "assistant", "content": response})
diff --git a/docassemble_demo/docassemble/demo/chatbot_bg_object.py b/docassemble_demo/docassemble/demo/chatbot_bg_object.py
index 68f88dd74..b10f644d7 100644
--- a/docassemble_demo/docassemble/demo/chatbot_bg_object.py
+++ b/docassemble_demo/docassemble/demo/chatbot_bg_object.py
@@ -1,6 +1,6 @@
# do not pre-load
import openai
-from docassemble.base.util import get_config, BackgroundAction, action_argument
+from docassemble.base.util import get_config, BackgroundAction
openai.api_key = get_config('openai key')
diff --git a/docassemble_demo/docassemble/demo/companydb.py b/docassemble_demo/docassemble/demo/companydb.py
index 1e5894efe..0cd40f34d 100644
--- a/docassemble_demo/docassemble/demo/companydb.py
+++ b/docassemble_demo/docassemble/demo/companydb.py
@@ -1,10 +1,24 @@
# do not pre-load
-# Import any DAObject classes or functions that you will need
-from docassemble.base.util import Individual, Person, DAObject, DAFileList, DAFile, Thing, as_datetime
-# Import the SQLObject and some associated utility functions
-from docassemble.base.sql import register_db, create_objects, SQLObject, SQLObjectRelationship, StandardRelationshipList
# Import SQLAlchemy names
from sqlalchemy import Column, ForeignKey, Integer, String, DateTime
+# Import any DAObject classes or functions that you will need
+from docassemble.base.util import (
+ Individual,
+ Person,
+ DAObject,
+ DAFileList,
+ DAFile,
+ Thing,
+ as_datetime,
+)
+# Import the SQLObject and some associated utility functions
+from docassemble.base.sql import (
+ register_db,
+ create_objects,
+ SQLObject,
+ SQLObjectRelationship,
+ StandardRelationshipList,
+)
# Only allow these names (DAObject classes) to be imported with a modules block
__all__ = ['Company', 'Shareholder', 'CompanyShareholder', 'Lawsuit', 'CompanyLawsuit', 'Document', 'LawsuitDocument', 'StandardRelationshipList']
diff --git a/docassemble_demo/docassemble/demo/contexttest.py b/docassemble_demo/docassemble/demo/contexttest.py
index ce68d8c5e..b1b0d3427 100644
--- a/docassemble_demo/docassemble/demo/contexttest.py
+++ b/docassemble_demo/docassemble/demo/contexttest.py
@@ -1,5 +1,5 @@
# do not pre-load
-from docassemble.base.util import current_context, Individual, log
+from docassemble.base.util import current_context, Individual
__all__ = ['AltIndividual']
diff --git a/docassemble_demo/docassemble/demo/custombg.py b/docassemble_demo/docassemble/demo/custombg.py
index d4a262f88..6a58021ff 100644
--- a/docassemble_demo/docassemble/demo/custombg.py
+++ b/docassemble_demo/docassemble/demo/custombg.py
@@ -1,13 +1,14 @@
# do not pre-load
-from docassemble.webapp.worker_common import workerapp, bg_context, worker_controller as wc
+from docassemble.webapp.tasks.app import celery_app
+from docassemble.webapp.tasks.context import bg_context
+from docassemble.base.util import comma_and_list
-
-@workerapp.task
+@celery_app.task
def custom_add_four(operand):
return operand + 4
-@workerapp.task
+@celery_app.task
def custom_comma_and_list(*pargs):
with bg_context():
- return wc.util.comma_and_list(*pargs)
+ return comma_and_list(*pargs)
diff --git a/docassemble_demo/docassemble/demo/data/questions/examples/stage-one.yml b/docassemble_demo/docassemble/demo/data/questions/examples/stage-one.yml
index 413bb11bf..5ca465f11 100644
--- a/docassemble_demo/docassemble/demo/data/questions/examples/stage-one.yml
+++ b/docassemble_demo/docassemble/demo/data/questions/examples/stage-one.yml
@@ -19,7 +19,7 @@ fields:
- Fruit: favorite_fruit
---
code: |
- part_two = user_info().package + ':data/questions/examples/stage-two.yml'
+ part_two = current_context().package + ':data/questions/examples/stage-two.yml'
---
code: |
user.name.first
diff --git a/docassemble_demo/docassemble/demo/data/templates/myinterviews.html b/docassemble_demo/docassemble/demo/data/templates/myinterviews.html
index 5771b7f3f..72444ffec 100644
--- a/docassemble_demo/docassemble/demo/data/templates/myinterviews.html
+++ b/docassemble_demo/docassemble/demo/data/templates/myinterviews.html
@@ -16,16 +16,16 @@ {{ title }}
{% for interview in interviews %}
- {% if interview['valid'] %}{{ interview['title'] }}{% else %}{{ interview['title'] }}{% endif %}
+ {% if interview['valid'] %}{{ interview['title'] }}{% else %}{{ interview['title'] }}{% endif %}
{{ interview['starttime'] }}
- {{ word('Delete') }}
+ {{ word('Delete') }}
{% endfor %}
{% if numinterviews > 0 %}
- {{ word('Delete All') }}
+ {{ word('Delete All') }}
{% endif %}
diff --git a/docassemble_demo/docassemble/demo/demodb.py b/docassemble_demo/docassemble/demo/demodb.py
index 6f523e481..c9c6a3542 100644
--- a/docassemble_demo/docassemble/demo/demodb.py
+++ b/docassemble_demo/docassemble/demo/demodb.py
@@ -1,10 +1,15 @@
# do not pre-load
+# Import SQLAlchemy names
+from sqlalchemy import Column, ForeignKey, Integer, String
# Import any DAObject classes that you will need
from docassemble.base.util import Individual, Person, DAObject
# Import the SQLObject and some associated utility functions
-from docassemble.base.sql import register_db, create_objects, SQLObject, SQLObjectRelationship
-# Import SQLAlchemy names
-from sqlalchemy import Column, ForeignKey, Integer, String
+from docassemble.base.sql import (
+ register_db,
+ create_objects,
+ SQLObject,
+ SQLObjectRelationship,
+)
# Only allow these names (DAObject classes) to be imported with a modules block
__all__ = ['Bank', 'Customer', 'BankCustomer']
diff --git a/docassemble_demo/docassemble/demo/ein.py b/docassemble_demo/docassemble/demo/ein.py
index 73913e810..79fe33bdb 100644
--- a/docassemble_demo/docassemble/demo/ein.py
+++ b/docassemble_demo/docassemble/demo/ein.py
@@ -4,4 +4,4 @@ def retrieve_ein(name):
return "54-54349343"
if name.text == "XYZ Incorporated":
return "32-84398493"
- raise Exception("Could not retrieve EIN for " + name.text)
+ raise ValueError("Could not retrieve EIN for " + name.text)
diff --git a/docassemble_demo/docassemble/demo/fruit_database.py b/docassemble_demo/docassemble/demo/fruit_database.py
index aaa1b085e..e6e21b3c2 100644
--- a/docassemble_demo/docassemble/demo/fruit_database.py
+++ b/docassemble_demo/docassemble/demo/fruit_database.py
@@ -24,7 +24,7 @@ def get_fruit_names():
def fruit_info(fruit):
if fruit not in fruit_info_by_name:
- raise Exception("Reference to invalid fruit " + fruit)
+ raise ValueError("Reference to invalid fruit " + fruit)
return fruit_info_by_name[fruit]
read_data('docassemble.demo:data/sources/fruit_data.xlsx')
diff --git a/docassemble_demo/docassemble/demo/gd_fetch.py b/docassemble_demo/docassemble/demo/gd_fetch.py
index 85b0fef33..e43747596 100644
--- a/docassemble_demo/docassemble/demo/gd_fetch.py
+++ b/docassemble_demo/docassemble/demo/gd_fetch.py
@@ -1,6 +1,6 @@
# do not pre-load
-from docassemble.base.util import DAGoogleAPI
import apiclient
+from docassemble.base.util import DAGoogleAPI
__all__ = ['fetch_file']
@@ -8,7 +8,7 @@
def fetch_file(file_id, path):
service = DAGoogleAPI().drive_service()
with open(path, 'wb') as fh:
- response = service.files().get_media(fileId=file_id)
+ response = service.files().get_media(fileId=file_id) # pylint: disable=no-member
downloader = apiclient.http.MediaIoBaseDownload(fh, response)
done = False
while done is False:
diff --git a/docassemble_demo/docassemble/demo/gettime.py b/docassemble_demo/docassemble/demo/gettime.py
index 9aa0efb33..2897dea39 100644
--- a/docassemble_demo/docassemble/demo/gettime.py
+++ b/docassemble_demo/docassemble/demo/gettime.py
@@ -3,7 +3,7 @@
def get_time():
- r = requests.get('http://worldclockapi.com/api/json/est/now')
+ r = requests.get('http://worldclockapi.com/api/json/est/now', timeout=60)
if r.status_code != 200:
- raise Exception("Could not obtain the time")
+ raise RuntimeError("Could not obtain the time")
return r.json()['currentDateTime']
diff --git a/docassemble_demo/docassemble/demo/google_drive.py b/docassemble_demo/docassemble/demo/google_drive.py
index f0d461581..55d17c136 100644
--- a/docassemble_demo/docassemble/demo/google_drive.py
+++ b/docassemble_demo/docassemble/demo/google_drive.py
@@ -1,6 +1,6 @@
# do not pre-load
-from docassemble.base.util import DAGoogleAPI, DAFile
import apiclient
+from docassemble.base.util import DAGoogleAPI, DAFile
api = DAGoogleAPI()
@@ -11,7 +11,7 @@ def get_folder_names():
service = api.drive_service()
items = []
while True:
- response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and sharedWithMe").execute()
+ response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and sharedWithMe").execute() # pylint: disable=no-member
for the_file in response.get('files', []):
items.append(the_file)
page_token = response.get('nextPageToken', None)
@@ -22,7 +22,7 @@ def get_folder_names():
def get_folder_id(folder_name):
service = api.drive_service()
- response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and sharedWithMe and name='" + str(folder_name) + "'").execute()
+ response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and sharedWithMe and name='" + str(folder_name) + "'").execute() # pylint: disable=no-member
folder_id = None
for item in response.get('files', []):
folder_id = item['id']
@@ -32,10 +32,10 @@ def get_folder_id(folder_name):
def get_file_id(filename, folder_name):
folder_id = get_folder_id(folder_name)
if folder_id is None:
- raise Exception("The folder was not found")
+ raise RuntimeError("The folder was not found")
service = api.drive_service()
file_id = None
- response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType!='application/vnd.google-apps.folder' and '" + str(folder_id) + "' in parents and name='" + str(filename) + "'").execute()
+ response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType!='application/vnd.google-apps.folder' and '" + str(folder_id) + "' in parents and name='" + str(filename) + "'").execute() # pylint: disable=no-member
for item in response.get('files', []):
file_id = item['id']
return file_id
@@ -44,11 +44,11 @@ def get_file_id(filename, folder_name):
def get_files_in_folder(folder_name):
folder_id = get_folder_id(folder_name)
if folder_id is None:
- raise Exception("The folder was not found")
+ raise RuntimeError("The folder was not found")
service = api.drive_service()
items = []
while True:
- response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType!='application/vnd.google-apps.folder' and trashed=false and '" + str(folder_id) + "' in parents").execute()
+ response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType!='application/vnd.google-apps.folder' and trashed=false and '" + str(folder_id) + "' in parents").execute() # pylint: disable=no-member
for the_file in response.get('files', []):
items.append(the_file)
page_token = response.get('nextPageToken', None)
@@ -60,11 +60,11 @@ def get_files_in_folder(folder_name):
def write_file_to_folder(path, mimetype, filename, folder_name):
folder_id = get_folder_id(folder_name)
if folder_id is None:
- raise Exception("The folder was not found")
+ raise RuntimeError("The folder was not found")
service = api.drive_service()
file_metadata = {'name': filename, 'parents': [folder_id]}
media = apiclient.http.MediaFileUpload(path, mimetype=mimetype)
- the_new_file = service.files().create(body=file_metadata,
+ the_new_file = service.files().create(body=file_metadata, # pylint: disable=no-member
media_body=media,
fields='id').execute()
return the_new_file.get('id')
@@ -73,13 +73,13 @@ def write_file_to_folder(path, mimetype, filename, folder_name):
def download_file(filename, folder_name):
file_id = get_file_id(filename, folder_name)
if file_id is None:
- raise Exception("The file was not found")
+ raise RuntimeError("The file was not found")
the_file = DAFile()
the_file.set_random_instance_name()
the_file.initialize(filename=filename)
service = api.drive_service()
with open(the_file.path(), 'wb') as fh:
- response = service.files().get_media(fileId=file_id)
+ response = service.files().get_media(fileId=file_id) # pylint: disable=no-member
downloader = apiclient.http.MediaIoBaseDownload(fh, response)
done = False
while done is False:
diff --git a/docassemble_demo/docassemble/demo/google_sheets.py b/docassemble_demo/docassemble/demo/google_sheets.py
index 7cdd62a33..f7e9295f6 100644
--- a/docassemble_demo/docassemble/demo/google_sheets.py
+++ b/docassemble_demo/docassemble/demo/google_sheets.py
@@ -1,11 +1,12 @@
# do not pre-load
import json
import gspread
-from docassemble.base.util import get_config
from oauth2client.service_account import ServiceAccountCredentials
+from docassemble.base.util import get_config
+
credential_json = get_config('google').get('service account credentials', None)
if credential_json is None:
- credential_info = None
+ credential_info = None # pylint: disable=invalid-name
else:
credential_info = json.loads(credential_json, strict=False)
diff --git a/docassemble_demo/docassemble/demo/google_vision2.py b/docassemble_demo/docassemble/demo/google_vision2.py
index 2b3366b31..173cab51c 100644
--- a/docassemble_demo/docassemble/demo/google_vision2.py
+++ b/docassemble_demo/docassemble/demo/google_vision2.py
@@ -1,13 +1,13 @@
# do not pre-load
import io
-import docassemble.base.util
from google.cloud import vision
+from docassemble.base.util import DAGoogleAPI
__all__ = ['gv_ocr']
def gv_ocr(the_file):
- api = docassemble.base.util.DAGoogleAPI()
+ api = DAGoogleAPI()
client = api.google_cloud_vision_client()
image = vision.Image()
with io.open(the_file.path(), 'rb') as image_file:
@@ -18,7 +18,7 @@ def gv_ocr(the_file):
for text in response.text_annotations:
output += text.description + "\n"
if response.error.message:
- raise Exception(response.error.message)
+ raise RuntimeError(response.error.message)
return output
# def async_detect_document(gcs_source_uri, gcs_destination_uri):
diff --git a/docassemble_demo/docassemble/demo/index.py b/docassemble_demo/docassemble/demo/index.py
index ee24253a0..5217697ea 100644
--- a/docassemble_demo/docassemble/demo/index.py
+++ b/docassemble_demo/docassemble/demo/index.py
@@ -1,5 +1,11 @@
# do not pre-load
-from docassemble.base.util import store_variables_snapshot, DAObject, current_context, start_time, variables_snapshot_connection
+from docassemble.base.util import (
+ store_variables_snapshot,
+ DAObject,
+ current_context,
+ start_time,
+ variables_snapshot_connection,
+)
__all__ = ['MyIndex']
diff --git a/docassemble_demo/docassemble/demo/oauthsheets.py b/docassemble_demo/docassemble/demo/oauthsheets.py
index c3ac1866c..bc984fe3a 100644
--- a/docassemble_demo/docassemble/demo/oauthsheets.py
+++ b/docassemble_demo/docassemble/demo/oauthsheets.py
@@ -1,10 +1,11 @@
# do not pre-load
-from docassemble.base.util import DAOAuth
from googleapiclient.discovery import build
+from docassemble.base.util import DAOAuth
__all__ = ['GoogleAuth']
+
class GoogleAuth(DAOAuth):
def init(self, *pargs, **kwargs):
@@ -16,10 +17,10 @@ def init(self, *pargs, **kwargs):
def test(self):
service = build('sheets', 'v4', http=self.get_http())
- SAMPLE_SPREADSHEET_ID = '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms'
- SAMPLE_RANGE_NAME = 'Class Data!A2:E'
sheet = service.spreadsheets()
- result = sheet.values().get(spreadsheetId=SAMPLE_SPREADSHEET_ID,
- range=SAMPLE_RANGE_NAME).execute()
+ sample_spreadsheet_id = '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms'
+ sample_range_name = 'Class Data!A2:E'
+ result = sheet.values().get(spreadsheetId=sample_spreadsheet_id,
+ range=sample_range_name).execute()
values = result.get('values', [])
return values
diff --git a/docassemble_demo/docassemble/demo/py.typed b/docassemble_demo/docassemble/demo/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/docassemble_demo/docassemble/demo/random-test.py b/docassemble_demo/docassemble/demo/random_test.py
similarity index 85%
rename from docassemble_demo/docassemble/demo/random-test.py
rename to docassemble_demo/docassemble/demo/random_test.py
index 78fda9a8e..22bb1a429 100755
--- a/docassemble_demo/docassemble/demo/random-test.py
+++ b/docassemble_demo/docassemble/demo/random_test.py
@@ -5,20 +5,20 @@
import string
import requests
-root = 'http://localhost'
-key = 'H3PLMKJKIVATLDPWHJH3AGWEJPFU5GRT'
+ROOT = 'http://localhost'
+KEY = 'H3PLMKJKIVATLDPWHJH3AGWEJPFU5GRT'
i = 'docassemble.demo:data/questions/questions.yml'
iterations = 100
while iterations:
- r = requests.get(root + '/api/session/new', params={'key': key, 'i': i})
+ r = requests.get(ROOT + '/api/session/new', params={'key': KEY, 'i': i}, timeout=60)
if r.status_code != 200:
sys.exit(r.text)
info = json.loads(r.text)
session = info['session']
secret = info['secret']
- r = requests.get(root + '/api/session/question', params={'key': key, 'i': i, 'secret': secret, 'session': session})
+ r = requests.get(ROOT + '/api/session/question', params={'key': KEY, 'i': i, 'secret': secret, 'session': session}, timeout=60)
if r.status_code != 200:
sys.exit(r.text)
info = json.loads(r.text)
@@ -65,11 +65,11 @@
elif field.get('datatype', None) == 'date':
variables[field['variable_name']] = "2015-04-15"
elif field.get('datatype', None) in ('file', 'files', 'camera', 'user', 'environment'):
- file_var_name = "file" + str(len(file_uploads))
+ file_var_name = "file" + str(len(file_uploads)) # pylint: disable=invalid-name
file_variables[file_var_name] = field['variable_name']
file_uploads[file_var_name] = open("data/static/art.jpg", "rb")
elif info['questionType'] == 'signature':
- file_var_name = "file" + str(len(file_uploads))
+ file_var_name = "file" + str(len(file_uploads)) # pylint: disable=invalid-name
file_variables[file_var_name] = field['variable_name']
file_uploads[file_var_name] = open("data/static/canvas.png", "rb")
elif field.get('datatype', None) == 'range':
@@ -83,20 +83,21 @@
sys.exit("Fields not recognized:\n" + repr(info['fields']))
sys.exit("Question not recognized:\n" + repr(info))
print("Session is " + session)
+ data = {}
if len(variables):
print("Setting variables:\n" + repr(variables))
- data = {'key': key, 'i': i, 'secret': secret, 'session': session, 'variables': json.dumps(variables)}
+ data = {'key': KEY, 'i': i, 'secret': secret, 'session': session, 'variables': json.dumps(variables)}
if len(file_variables):
- data = {'key': key, 'i': i, 'secret': secret, 'session': session}
+ data = {'key': KEY, 'i': i, 'secret': secret, 'session': session}
data['question_name'] = info['questionName']
if 'event_list' in info:
data['event_list'] = json.dumps(info['event_list'])
if len(file_uploads):
print("Setting file variables:\n" + repr(file_variables))
data['file_variables'] = json.dumps(file_variables)
- r = requests.post(root + '/api/session', data=data, files=file_uploads)
+ r = requests.post(ROOT + '/api/session', data=data, files=file_uploads, timeout=60)
else:
- r = requests.post(root + '/api/session', data=data)
+ r = requests.post(ROOT + '/api/session', data=data, timeout=60)
if r.status_code != 200:
sys.exit(r.text)
print("Got question:\n" + r.text)
@@ -107,7 +108,7 @@
sys.exit(r.text)
steps += 1
- # r = requests.delete(root + '/api/session', params={'key': key, 'i': i, 'session': session})
+ # r = requests.delete(ROOT + '/api/session', params={'key': KEY, 'i': i, 'session': session})
# if r.status_code != 204:
# sys.exit(r.text)
diff --git a/docassemble_demo/docassemble/demo/reldemodb.py b/docassemble_demo/docassemble/demo/reldemodb.py
index a8523aa3e..9fda55e34 100644
--- a/docassemble_demo/docassemble/demo/reldemodb.py
+++ b/docassemble_demo/docassemble/demo/reldemodb.py
@@ -1,10 +1,10 @@
# do not pre-load
-# Import any DAObject classes that you will need
-from docassemble.base.util import Individual, Person, DAObject
-# Import the SQLObject and some associated utility functions
-from docassemble.base.sql import register_db, create_objects, SQLObject
# Import SQLAlchemy names
from sqlalchemy import Column, ForeignKey, Integer, String
+# Import the SQLObject and some associated utility functions
+from docassemble.base.sql import register_db, create_objects, SQLObject
+# Import any DAObject classes that you will need
+from docassemble.base.util import Individual, Person, DAObject
# Only allow these names (DAObject classes) to be imported with a modules block
__all__ = ['Bank', 'Customer', 'BankCustomer']
diff --git a/docassemble_demo/docassemble/demo/sign.py b/docassemble_demo/docassemble/demo/sign.py
index 2c44a2548..391d7c346 100644
--- a/docassemble_demo/docassemble/demo/sign.py
+++ b/docassemble_demo/docassemble/demo/sign.py
@@ -1,7 +1,29 @@
# do not pre-load
import random
import string
-from docassemble.base.util import DAObject, DAList, DAFileCollection, interview_url_action, DADict, word, today, current_datetime, Person, DAEmailRecipient, comma_and_list, interface, value, send_email, background_action, reconsider, force_ask, background_response, device, prevent_going_back
+from docassemble.base.error import DAException
+from docassemble.base.util import (
+ DAObject,
+ DAList,
+ DAFileCollection,
+ interview_url_action,
+ DADict,
+ word,
+ today,
+ current_datetime,
+ Person,
+ DAEmailRecipient,
+ comma_and_list,
+ interface,
+ value,
+ send_email,
+ background_action,
+ reconsider,
+ force_ask,
+ background_response,
+ device,
+ prevent_going_back,
+)
__all__ = ['SigningProcess']
@@ -32,14 +54,14 @@ def rationalize(self):
self.documents = [self.documents]
for document in self.documents:
if not isinstance(document, str):
- raise Exception("SigningProcess.notify: the document references must consist of text strings only")
+ raise DAException("SigningProcess.notify: the document references must consist of text strings only")
if not isinstance(value(document), DAFileCollection):
- raise Exception("SigningProcess.notify: the document references must refer to DAFileCollection objects only")
+ raise DAException("SigningProcess.notify: the document references must refer to DAFileCollection objects only")
if not isinstance(self.additional_people_to_notify, (list, DAList)):
self.additional_people_to_notify = [self.additional_people_to_notify]
for person in self.additional_people_to_notify:
if not isinstance(person, (Person, DAEmailRecipient)):
- raise Exception("SigningProcess: an additional person to notify must be a person")
+ raise DAException("SigningProcess: an additional person to notify must be a person")
def out_for_signature(self):
if self.initial_notification_triggered or self.initial_notification_sent:
@@ -98,8 +120,8 @@ def final_notify(self):
def _verify_signer(self, signer):
if not isinstance(signer, Person):
if hasattr(signer, 'instanceName'):
- raise Exception("There was a reference to a signer " + signer.instanceName + " that is not a person.")
- raise Exception("There was a reference to a signer that is not a person.")
+ raise DAException("There was a reference to a signer " + signer.instanceName + " that is not a person.")
+ raise DAException("There was a reference to a signer that is not a person.")
if signer not in [y['signer'] for y in self.info_by_code.values()]:
code = ''.join(random.choice(string.ascii_lowercase) for i in range(10))
self.info_by_code[code] = {'signed': False, 'signer': signer}
@@ -109,7 +131,7 @@ def _code_for(self, signer):
for code, info in self.info_by_code.items():
if info['signer'] is signer:
return code
- raise Exception("No code existed for signer")
+ raise DAException("No code existed for signer")
def has_signed(self, signer):
code = self._code_for(signer)
@@ -154,7 +176,7 @@ def collect_signature(self, code):
def validate_signature(self, code):
if code not in self.info_by_code:
- raise Exception("Invalid code")
+ raise DAException("Invalid code")
self.info_by_code[code]['signed'] = True
self.info_by_code[code]['date'] = today()
self.info_by_code[code]['datetime'] = current_datetime()
diff --git a/docassemble_demo/docassemble/demo/ssn.py b/docassemble_demo/docassemble/demo/ssn.py
index 6c8a4e1c6..0745c70d9 100644
--- a/docassemble_demo/docassemble/demo/ssn.py
+++ b/docassemble_demo/docassemble/demo/ssn.py
@@ -9,14 +9,14 @@ class SSN(CustomDataType):
input_class = 'da-ssn'
javascript = """\
$.validator.addMethod('ssn', function(value, element, params){
- return value == '' || /^[0-9]{3}\-?[0-9]{2}\-?[0-9]{4}$/.test(value);
+ return value == '' || /^[0-9]{3}-?[0-9]{2}-?[0-9]{4}$/.test(value);
});
-""" # noqa: W605
+"""
jq_rule = 'ssn'
jq_message = 'You need to enter a valid SSN.'
@classmethod
- def validate(cls, item, variable_name, data): # pylint: disable=unused-argument
+ def validate(cls, item, variable_name, data): # pylint: disable=unused-argument, arguments-differ
item = str(item).strip()
m = re.search(r'^[0-9]{3}-?[0-9]{2}-?[0-9]{4}$', item)
if item == '' or m:
@@ -24,7 +24,7 @@ def validate(cls, item, variable_name, data): # pylint: disable=unused-argument
raise DAValidationError("A SSN needs to be in the form xxx-xx-xxxx")
@classmethod
- def transform(cls, item, variable_name, data): # pylint: disable=unused-argument
+ def transform(cls, item, variable_name, data): # pylint: disable=unused-argument, arguments-differ
item = str(item).strip()
m = re.search(r'^([0-9]{3})-?([0-9]{2})-?([0-9]{4})$', item)
if m:
diff --git a/docassemble_demo/docassemble/demo/testcustombg.py b/docassemble_demo/docassemble/demo/testcustombg.py
index e4f076344..400167f8b 100644
--- a/docassemble_demo/docassemble/demo/testcustombg.py
+++ b/docassemble_demo/docassemble/demo/testcustombg.py
@@ -1,9 +1,11 @@
# do not pre-load
from flask import request, jsonify
from flask_cors import cross_origin
-from docassemble.webapp.app_object import app, csrf
-from docassemble.webapp.server import api_verify, jsonify_with_status
-from docassemble.webapp.worker_common import workerapp
+from docassemble.webapp.extensions import csrf
+from docassemble.webapp.app_object import flaskapp as app
+from docassemble.webapp.api.helpers import api_verify
+from docassemble.webapp.utils.helpers import jsonify_with_status
+from docassemble.webapp.tasks.app import celery_app
from docassemble.base.config import in_celery
if not in_celery:
from docassemble.demo.custombg import custom_add_four, custom_comma_and_list
@@ -19,7 +21,7 @@ def start_process():
operand = int(request.args['operand'])
except:
return jsonify_with_status({"success": False, "error_message": "Missing or invalid operand."}, 400)
- task = custom_add_four.delay(operand)
+ task = custom_add_four.delay(operand) # pylint: disable=possibly-used-before-assignment
return jsonify({"success": True, 'task_id': task.id})
@@ -30,7 +32,7 @@ def poll_for_result():
if not api_verify():
return jsonify_with_status({"success": False, "error_message": "Access denied."}, 403)
try:
- result = workerapp.AsyncResult(id=request.args['task_id'])
+ result = celery_app.AsyncResult(id=request.args['task_id'])
except:
return jsonify_with_status({"success": False, "error_message": "Invalid task_id."}, 400)
if not result.ready():
@@ -46,5 +48,5 @@ def poll_for_result():
def start_process_2():
if not api_verify():
return jsonify_with_status({"success": False, "error_message": "Access denied."}, 403)
- task = custom_comma_and_list.delay('foo', 'bar', 'foobar')
+ task = custom_comma_and_list.delay('foo', 'bar', 'foobar') # pylint: disable=possibly-used-before-assignment
return jsonify({"success": True, 'task_id': task.id})
diff --git a/docassemble_demo/docassemble/demo/translate.py b/docassemble_demo/docassemble/demo/translate.py
index 6d3ee9475..b3e4a3f2e 100644
--- a/docassemble_demo/docassemble/demo/translate.py
+++ b/docassemble_demo/docassemble/demo/translate.py
@@ -1,7 +1,7 @@
# do not pre-load
import re
-from docassemble.base.util import log, get_config
from googleapiclient.discovery import build
+from docassemble.base.util import log, get_config
__all__ = ['translate_phrase']
@@ -15,7 +15,7 @@ def translate_phrase(phrase, source_language, target_language):
try:
service = build('translate', 'v2',
developerKey=api_key)
- resp = service.translations().list(
+ resp = service.translations().list( # pylint: disable=no-member
source=source_language,
target=target_language,
q=[phrase]
diff --git a/docassemble_demo/docassemble/demo/user_permissions.py b/docassemble_demo/docassemble/demo/user_permissions.py
index b86323d75..630b512f8 100644
--- a/docassemble_demo/docassemble/demo/user_permissions.py
+++ b/docassemble_demo/docassemble/demo/user_permissions.py
@@ -1,14 +1,21 @@
# do not pre-load
import string
import random
-from docassemble.base.util import interview_list, create_user, get_user_list, get_user_info, set_user_info, manage_privileges
+from docassemble.base.util import (
+ interview_list,
+ create_user,
+ get_user_list,
+ get_user_info,
+ set_user_info,
+ manage_privileges,
+)
__all__ = ['get_permissions']
r = random.SystemRandom()
-other_user = 29
-other_interview = 'docassemble.base:data/questions/examples/madlibs.yml'
+OTHER_USER = 29
+OTHER_INTERVIEW = 'docassemble.base:data/questions/examples/madlibs.yml'
def random_alphanumeric(length):
@@ -22,23 +29,23 @@ def random_lower_string(length):
def get_permissions():
results = {}
try:
- result = interview_list(user_id='all', filename=other_interview)
+ result = interview_list(user_id='all', filename=OTHER_INTERVIEW)
assert result is not None
- result = interview_list(user_id=other_user, filename=other_interview)
+ result = interview_list(user_id=OTHER_USER, filename=OTHER_INTERVIEW)
assert result is not None
results['access_sessions'] = True
except BaseException as err:
results['access_sessions'] = err.__class__.__name__ + ": " + str(err)
try:
- result = interview_list(user_id=other_user, action='delete_all', filename=other_interview)
+ result = interview_list(user_id=OTHER_USER, action='delete_all', filename=OTHER_INTERVIEW)
assert result is not None
- result = interview_list(user_id='all', action='delete_all', filename=other_interview)
+ result = interview_list(user_id='all', action='delete_all', filename=OTHER_INTERVIEW)
assert result is not None
results['edit_sessions'] = True
except BaseException as err:
results['edit_sessions'] = err.__class__.__name__ + ": " + str(err)
try:
- get_user_info(user_id=other_user)
+ get_user_info(user_id=OTHER_USER)
user_list = []
next_id = None
while True:
@@ -56,24 +63,24 @@ def get_permissions():
except BaseException as err:
results['create_user'] = err.__class__.__name__ + ": " + str(err)
try:
- set_user_info(user_id=other_user, first_name='Foo', last_name='Bar')
+ set_user_info(user_id=OTHER_USER, first_name='Foo', last_name='Bar')
results['edit_user_info'] = True
except BaseException as err:
results['edit_user_info'] = err.__class__.__name__ + ": " + str(err)
try:
- set_user_info(user_id=other_user, password=random_lower_string(8))
+ set_user_info(user_id=OTHER_USER, password=random_lower_string(8))
results['edit_user_password'] = True
except BaseException as err:
results['edit_user_password'] = err.__class__.__name__ + ": " + str(err)
try:
- set_user_info(user_id=other_user, active=False)
- set_user_info(user_id=other_user, active=True)
+ set_user_info(user_id=OTHER_USER, active=False)
+ set_user_info(user_id=OTHER_USER, active=True)
results['edit_user_active_status'] = True
except BaseException as err:
results['edit_user_active_status'] = err.__class__.__name__ + ": " + str(err)
try:
- set_user_info(user_id=other_user, privileges=['user', 'trainer', 'customer'])
- set_user_info(user_id=other_user, privileges=['user', 'trainer'])
+ set_user_info(user_id=OTHER_USER, privileges=['user', 'trainer', 'customer'])
+ set_user_info(user_id=OTHER_USER, privileges=['user', 'trainer'])
results['edit_user_privileges'] = True
except BaseException as err:
results['edit_user_privileges'] = err.__class__.__name__ + ": " + str(err)
diff --git a/docassemble_demo/py.typed b/docassemble_demo/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/docassemble_demo/pyproject.toml b/docassemble_demo/pyproject.toml
index 16b8d8796..155abd264 100644
--- a/docassemble_demo/pyproject.toml
+++ b/docassemble_demo/pyproject.toml
@@ -3,7 +3,7 @@ requires = ["setuptools==82.0.1"]
build-backend = "setuptools.build_meta"
[project]
name = "docassemble.demo"
-version = '1.9.11'
+version = '1.10.1'
description = "A demonstration package for docassemble."
readme = "README.md"
authors = [
@@ -13,7 +13,7 @@ license = "MIT"
license-files = ["LICENSE.txt"]
requires-python = ">= 3.9"
dependencies = [
- 'docassemble.base==1.9.11'
+ 'docassemble.base==1.10.1'
# "ics",
# "openai",
# "anthropic",
@@ -28,3 +28,50 @@ Documentation = "https://docassemble.org/docs.html"
Repository = "https://github.com/jhpyle/docassemble.git"
"Bug Tracker" = "https://github.com/jhpyle/docassemble/issues"
Changelog = "https://github.com/jhpyle/docassemble/blob/master/CHANGELOG.md"
+[tool.ruff]
+src = ["docassemble_base", "docassemble_webapp", "docassemble_demo"]
+
+[tool.ruff.lint]
+ignore = ["E722", "E226", "E24", "E501", "C901", "E305", "E306"]
+
+[tool.pylint.main]
+init-hook = """
+import sys
+sys.path.insert(0, '../docassemble_webapp')
+sys.path.insert(0, '../docassemble_base')
+sys.path.insert(0, '../docassemble_demo')
+"""
+
+[tool.pylint.messages_control]
+disable = [
+ "attribute-defined-outside-init",
+ "bare-except",
+ "broad-exception-caught",
+ "consider-using-f-string",
+ "consider-using-with",
+ "eval-used",
+ "exec-used",
+ "import-outside-toplevel",
+ "line-too-long",
+ "missing-class-docstring",
+ "missing-function-docstring",
+ "missing-module-docstring",
+ "no-member",
+ "raise-missing-from",
+ "too-few-public-methods",
+ "too-many-arguments",
+ "too-many-boolean-expressions",
+ "too-many-branches",
+ "too-many-function-args",
+ "too-many-lines",
+ "too-many-locals",
+ "too-many-nested-blocks",
+ "too-many-positional-arguments",
+ "too-many-public-methods",
+ "too-many-return-statements",
+ "too-many-statements",
+ "too-many-instance-attributes",
+]
+
+[tool.mypy]
+disable_error_code = ["var-annotated"]
\ No newline at end of file
diff --git a/docassemble_webapp/LICENSE.txt b/docassemble_webapp/LICENSE.txt
index f9a98fdcf..a3c07394e 100644
--- a/docassemble_webapp/LICENSE.txt
+++ b/docassemble_webapp/LICENSE.txt
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2015-2025 Jonathan Pyle
+Copyright (c) 2015-2026 Jonathan Pyle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/docassemble_webapp/MANIFEST.in b/docassemble_webapp/MANIFEST.in
index 5c03b3b84..00e852612 100644
--- a/docassemble_webapp/MANIFEST.in
+++ b/docassemble_webapp/MANIFEST.in
@@ -2,7 +2,18 @@ include README.md
graft docassemble/webapp/data
graft docassemble/webapp/alembic
include docassemble/webapp/alembic.ini
+graft docassemble/webapp/admin/templates
+graft docassemble/webapp/api/templates
+graft docassemble/webapp/auth/templates
+graft docassemble/webapp/develop/templates
+graft docassemble/webapp/logs/templates
+graft docassemble/webapp/main/templates
+graft docassemble/webapp/ml/templates
+graft docassemble/webapp/monitor/templates
+graft docassemble/webapp/packages/templates
+graft docassemble/webapp/phonelogin/templates
graft docassemble/webapp/templates
+graft docassemble/webapp/users/templates
graft docassemble/webapp/static
recursive-exclude * *.egg-info
recursive-exclude .git *
diff --git a/docassemble_webapp/docassemble/webapp/__init__.py b/docassemble_webapp/docassemble/webapp/__init__.py
index b95b03c16..a0865bba6 100644
--- a/docassemble_webapp/docassemble/webapp/__init__.py
+++ b/docassemble_webapp/docassemble/webapp/__init__.py
@@ -1 +1 @@
-__version__ = "1.9.11"
+__version__ = "1.10.1"
diff --git a/docassemble_webapp/docassemble/webapp/admin/__init__.py b/docassemble_webapp/docassemble/webapp/admin/__init__.py
new file mode 100644
index 000000000..48bce53c4
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/admin/__init__.py
@@ -0,0 +1 @@
+from .views import admin_bp # noqa: F401
diff --git a/docassemble_webapp/docassemble/webapp/admin/forms.py b/docassemble_webapp/docassemble/webapp/admin/forms.py
new file mode 100644
index 000000000..98a45867f
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/admin/forms.py
@@ -0,0 +1,21 @@
+from flask_wtf import FlaskForm
+from wtforms import (
+ StringField,
+ SubmitField,
+ TextAreaField,
+)
+from docassemble.base.language.words import LazyWord as word
+
+
+class ConfigForm(FlaskForm):
+ config_content = TextAreaField(word('Configuration YAML'))
+ submit = SubmitField(word('Save'))
+ cancel = SubmitField(word('Cancel'))
+
+
+class InterviewsListForm(FlaskForm):
+ i = StringField()
+ session = StringField()
+ tags = StringField()
+ delete = SubmitField()
+ delete_all = SubmitField()
diff --git a/docassemble_webapp/docassemble/webapp/admin/funcs.py b/docassemble_webapp/docassemble/webapp/admin/funcs.py
new file mode 100644
index 000000000..413ea9200
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/admin/funcs.py
@@ -0,0 +1,59 @@
+import copy
+from flask_login import current_user
+from docassemble.base.interview_cache import get_interview
+from docassemble.base.language.words import word
+from docassemble.webapp.config import daconfig
+from docassemble.webapp.hooks.impl import hookimpl
+from docassemble.webapp.utils.hooks import url_for
+from docassemble.webapp.utils.logger import logmessage
+
+@hookimpl(specname='server_interview_menu')
+def interview_menu(absolute_urls, start_new, tag):
+ interview_info = []
+ for key, yaml_filename in sorted(daconfig['dispatch'].items()):
+ try:
+ interview = get_interview(yaml_filename)
+ if interview.is_unlisted():
+ continue
+ if current_user.is_anonymous:
+ if not interview.allowed_to_see_listed(is_anonymous=True):
+ continue
+ else:
+ if not interview.allowed_to_see_listed(has_roles=[role.name for role in current_user.roles]):
+ continue
+ if interview.source is None:
+ package = None
+ else:
+ package = interview.source.get_package()
+ titles = interview.get_title({'_internal': {}})
+ tags = interview.get_tags({'_internal': {}})
+ metadata = copy.deepcopy(interview.consolidated_metadata)
+ if 'tags' in metadata:
+ del metadata['tags']
+ interview_title = titles.get('full', titles.get('short', word('Untitled')))
+ subtitle = titles.get('sub', None)
+ status_class = None
+ subtitle_class = None
+ except:
+ interview_title = yaml_filename
+ tags = set()
+ metadata = {}
+ package = None
+ subtitle = None
+ status_class = 'dainterviewhaserror'
+ subtitle_class = 'dainvisible'
+ logmessage("interview_dispatch: unable to load interview file " + yaml_filename)
+ if tag is not None and tag not in tags:
+ continue
+ if absolute_urls:
+ if start_new:
+ url = url_for('interview.run_interview', dispatch=key, _external=True, reset='1')
+ else:
+ url = url_for('interview.redirect_to_interview', dispatch=key, _external=True)
+ else:
+ if start_new:
+ url = url_for('interview.run_interview', dispatch=key, reset='1')
+ else:
+ url = url_for('interview.redirect_to_interview', dispatch=key)
+ interview_info.append({'link': url, 'title': interview_title, 'status_class': status_class, 'subtitle': subtitle, 'subtitle_class': subtitle_class, 'filename': yaml_filename, 'package': package, 'tags': sorted(tags), 'metadata': metadata})
+ return interview_info
diff --git a/docassemble_webapp/docassemble/webapp/templates/pages/config.html b/docassemble_webapp/docassemble/webapp/admin/templates/admin/config.html
similarity index 100%
rename from docassemble_webapp/docassemble/webapp/templates/pages/config.html
rename to docassemble_webapp/docassemble/webapp/admin/templates/admin/config.html
diff --git a/docassemble_webapp/docassemble/webapp/templates/pages/interviews.html b/docassemble_webapp/docassemble/webapp/admin/templates/admin/interviews.html
similarity index 87%
rename from docassemble_webapp/docassemble/webapp/templates/pages/interviews.html
rename to docassemble_webapp/docassemble/webapp/admin/templates/admin/interviews.html
index e324bd93d..5ddee06e5 100644
--- a/docassemble_webapp/docassemble/webapp/templates/pages/interviews.html
+++ b/docassemble_webapp/docassemble/webapp/admin/templates/admin/interviews.html
@@ -8,7 +8,7 @@
{%- block content %}
{{ get_part('interview page heading', word('Resume an interview')) }}
{{ get_part('interview page pre') }}
-
+
{{ form.hidden_tag() }}
{%- if tag %}
@@ -42,7 +42,7 @@
{{ get_part('interview page headin
{%- for interview in interviews %}
{%- if not interview['metadata'].get('hidden', False) %}
- {% if interview['valid'] %}{{ interview['title'] | safe }}{% else %}{{ interview['title'] | safe }}{% endif %}{% if interview['subtitle'] %}
{{ interview['subtitle'] | safe}}{% endif %}
+ {% if interview['valid'] %}{{ interview['title'] | safe }}{% else %}{{ interview['title'] | safe }}{% endif %}{% if interview['subtitle'] %}
{{ interview['subtitle'] | safe}}{% endif %}
{%- if tags_used %}
{% for tag in interview['tags'] %}{{ tag }} {% endfor %}
{%- endif %}
@@ -70,7 +70,7 @@ {{ get_part('interview page headin
{% endfor %}
@@ -40,7 +40,7 @@
{%- endblock %}
{%- block bottom %}
{%- if mode == 'standard' %}
-
+
{%- endif %}
{%- endblock %}
diff --git a/docassemble_webapp/docassemble/webapp/templates/pages/manage_projects.html b/docassemble_webapp/docassemble/webapp/develop/templates/develop/manage_projects.html
similarity index 66%
rename from docassemble_webapp/docassemble/webapp/templates/pages/manage_projects.html
rename to docassemble_webapp/docassemble/webapp/develop/templates/develop/manage_projects.html
index ed4434d38..16fed2ff1 100644
--- a/docassemble_webapp/docassemble/webapp/templates/pages/manage_projects.html
+++ b/docassemble_webapp/docassemble/webapp/develop/templates/develop/manage_projects.html
@@ -15,7 +15,7 @@
{%- endif %}
@@ -28,8 +28,8 @@
{%- for project in projects %}
- {{ project }}
- {{ word('Rename') }} {{ word('Delete') }}
+ {{ project }}
+ {{ word('Rename') }} {{ word('Delete') }}
{% endfor %}
@@ -42,11 +42,11 @@
{%- endif %}
-
+
{{ form.csrf_token }}
{{ render_field(form.name) }}
{{ render_submit_field(form.submit) }}
- Cancel
+ Cancel
{%- elif mode == 'rename' %}
{%- if description %}
@@ -56,11 +56,11 @@
{%- endif %}
-
+
{{ form.csrf_token }}
{{ render_field(form.name) }}
{{ render_submit_field(form.submit) }}
- Cancel
+ Cancel
{%- elif mode == 'delete' %}
{%- if description %}
@@ -70,10 +70,10 @@
{%- endif %}
-
+
{{ form.csrf_token }}
{{ render_submit_field(form.submit) }}
- Cancel
+ Cancel
{%- endif %}
{%- if after_text %}
@@ -84,7 +84,7 @@
{%- endblock %}
{%- block bottom %}
{%- if mode == 'standard' %}
-
+
{%- endif %}
{%- endblock %}
diff --git a/docassemble_webapp/docassemble/webapp/templates/pages/od_sync_wait.html b/docassemble_webapp/docassemble/webapp/develop/templates/develop/od_sync_wait.html
similarity index 100%
rename from docassemble_webapp/docassemble/webapp/templates/pages/od_sync_wait.html
rename to docassemble_webapp/docassemble/webapp/develop/templates/develop/od_sync_wait.html
diff --git a/docassemble_webapp/docassemble/webapp/templates/pages/officeaddin.html b/docassemble_webapp/docassemble/webapp/develop/templates/develop/officeaddin.html
similarity index 100%
rename from docassemble_webapp/docassemble/webapp/templates/pages/officeaddin.html
rename to docassemble_webapp/docassemble/webapp/develop/templates/develop/officeaddin.html
diff --git a/docassemble_webapp/docassemble/webapp/templates/pages/officefunctionfile.html b/docassemble_webapp/docassemble/webapp/develop/templates/develop/officefunctionfile.html
similarity index 100%
rename from docassemble_webapp/docassemble/webapp/templates/pages/officefunctionfile.html
rename to docassemble_webapp/docassemble/webapp/develop/templates/develop/officefunctionfile.html
diff --git a/docassemble_webapp/docassemble/webapp/templates/pages/officemanifest.xml b/docassemble_webapp/docassemble/webapp/develop/templates/develop/officemanifest.xml
similarity index 100%
rename from docassemble_webapp/docassemble/webapp/templates/pages/officemanifest.xml
rename to docassemble_webapp/docassemble/webapp/develop/templates/develop/officemanifest.xml
diff --git a/docassemble_webapp/docassemble/webapp/templates/pages/officeouter.html b/docassemble_webapp/docassemble/webapp/develop/templates/develop/officeouter.html
similarity index 97%
rename from docassemble_webapp/docassemble/webapp/templates/pages/officeouter.html
rename to docassemble_webapp/docassemble/webapp/develop/templates/develop/officeouter.html
index c77f3259a..6628e2a65 100644
--- a/docassemble_webapp/docassemble/webapp/templates/pages/officeouter.html
+++ b/docassemble_webapp/docassemble/webapp/develop/templates/develop/officeouter.html
@@ -18,7 +18,7 @@
-
+
@@ -59,7 +59,7 @@
"""
+ response = make_response(render_template('develop/playground_poll.html', version_warning=None, bodyclass='daadminbody', extra_js=Markup(script), tab_title=word('Waiting'), page_title=word('Waiting')), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+def get_gd_flow():
+ app_credentials = current_app.config['OAUTH_CREDENTIALS'].get('googledrive', {})
+ client_id = app_credentials.get('id', None)
+ client_secret = app_credentials.get('secret', None)
+ if client_id is None or client_secret is None:
+ raise DAError('Google Drive is not configured.')
+ flow = oauth2client.client.OAuth2WebServerFlow(
+ client_id=client_id,
+ client_secret=client_secret,
+ scope='https://www.googleapis.com/auth/drive',
+ redirect_uri=url_for('develop.google_drive_callback', _external=True),
+ access_type='offline',
+ prompt='consent')
+ return flow
+
+
+def get_gd_folder():
+ key = 'da:googledrive:mapping:userid:' + str(current_user.id)
+ folder = r.get(key)
+ if folder is not None:
+ return folder.decode()
+ return folder
+
+
+def set_gd_folder(folder):
+ key = 'da:googledrive:mapping:userid:' + str(current_user.id)
+ if folder is None:
+ r.delete(key)
+ else:
+ set_od_folder(None)
+ r.set(key, folder)
+
+
+def get_od_flow():
+ app_credentials = current_app.config['OAUTH_CREDENTIALS'].get('onedrive', {})
+ client_id = app_credentials.get('id', None)
+ client_secret = app_credentials.get('secret', None)
+ if client_id is None or client_secret is None:
+ raise DAError('OneDrive is not configured.')
+ flow = oauth2client.client.OAuth2WebServerFlow(
+ client_id=client_id,
+ client_secret=client_secret,
+ scope='files.readwrite.all user.read offline_access',
+ redirect_uri=url_for('develop.onedrive_callback', _external=True),
+ response_type='code',
+ auth_uri='https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
+ token_uri='https://login.microsoftonline.com/common/oauth2/v2.0/token')
+ return flow
+
+
+def get_od_folder():
+ key = 'da:onedrive:mapping:userid:' + str(current_user.id)
+ folder = r.get(key)
+ if folder is not None:
+ return folder.decode()
+ return folder
+
+
+def set_od_folder(folder):
+ key = 'da:onedrive:mapping:userid:' + str(current_user.id)
+ if folder is None:
+ r.delete(key)
+ else:
+ set_gd_folder(None)
+ r.set(key, folder)
+
+
+@develop_bp.route('/google_drive_callback', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def google_drive_callback():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ for key in request.args:
+ logmessage("google_drive_callback: argument " + str(key) + ": " + str(request.args[key]))
+ if 'code' in request.args:
+ flow = get_gd_flow()
+ credentials = flow.step2_exchange(request.args['code'])
+ storage = RedisCredStorage(oauth_app='googledrive')
+ storage.put(credentials)
+ error = None
+ elif 'error' in request.args:
+ error = request.args['error']
+ else:
+ error = word("could not connect to Google Drive")
+ if error:
+ flash(word('There was a Google Drive error: ' + error), 'error')
+ return redirect(url_for('user.profile'))
+ flash(word('Connected to Google Drive'), 'success')
+ return redirect(url_for('develop.google_drive_page'))
+
+
+def rename_gd_project(old_project, new_project):
+ the_folder = get_gd_folder()
+ if the_folder is None:
+ logmessage('rename_gd_project: folder not configured')
+ return False
+ storage = RedisCredStorage(oauth_app='googledrive')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ logmessage('rename_gd_project: credentials missing or expired')
+ return False
+ http = credentials.authorize(httplib2.Http())
+ import apiclient # pylint: disable=import-outside-toplevel
+ service = apiclient.discovery.build('drive', 'v3', http=http)
+ response = service.files().get(fileId=the_folder, fields="mimeType, id, name, trashed").execute() # pylint: disable=no-member
+ trashed = response.get('trashed', False)
+ the_mime_type = response.get('mimeType', None)
+ if trashed is True or the_mime_type != "application/vnd.google-apps.folder":
+ logmessage('rename_gd_project: folder did not exist')
+ return False
+ for section in ['static', 'templates', 'questions', 'modules', 'sources', 'packages']:
+ logmessage("rename_gd_project: section is " + section)
+ subdir = None
+ page_token = None
+ while True:
+ response = service.files().list(spaces="drive", pageToken=page_token, fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and trashed=false and name='" + str(section) + "' and '" + str(the_folder) + "' in parents").execute() # pylint: disable=no-member
+ for the_file in response.get('files', []):
+ if 'id' in the_file:
+ subdir = the_file['id']
+ break
+ page_token = response.get('nextPageToken', None)
+ if subdir is not None or page_token is None:
+ break
+ if subdir is None:
+ logmessage('rename_gd_project: section ' + str(section) + ' could not be found')
+ continue
+ subsubdir = None
+ page_token = None
+ while True:
+ response = service.files().list(spaces="drive", pageToken=page_token, fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and trashed=false and name='" + str(old_project) + "' and '" + str(subdir) + "' in parents").execute() # pylint: disable=no-member
+ for the_file in response.get('files', []):
+ if 'id' in the_file:
+ subsubdir = the_file['id']
+ break
+ page_token = response.get('nextPageToken', None)
+ if subsubdir is not None or page_token is None:
+ break
+ if subsubdir is None:
+ logmessage('rename_gd_project: project ' + str(old_project) + ' could not be found in ' + str(section))
+ continue
+ metadata = {'name': new_project}
+ service.files().update(fileId=subsubdir, body=metadata, fields='name').execute() # pylint: disable=no-member
+ logmessage('rename_gd_project: folder ' + str(old_project) + ' renamed in section ' + str(section))
+ return True
+
+
+def trash_gd_project(old_project):
+ the_folder = get_gd_folder()
+ if the_folder is None:
+ logmessage('trash_gd_project: folder not configured')
+ return False
+ storage = RedisCredStorage(oauth_app='googledrive')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ logmessage('trash_gd_project: credentials missing or expired')
+ return False
+ http = credentials.authorize(httplib2.Http())
+ import apiclient # pylint: disable=import-outside-toplevel
+ service = apiclient.discovery.build('drive', 'v3', http=http)
+ response = service.files().get(fileId=the_folder, fields="mimeType, id, name, trashed").execute() # pylint: disable=no-member
+ trashed = response.get('trashed', False)
+ the_mime_type = response.get('mimeType', None)
+ if trashed is True or the_mime_type != "application/vnd.google-apps.folder":
+ logmessage('trash_gd_project: folder did not exist')
+ return False
+ for section in ['static', 'templates', 'questions', 'modules', 'sources', 'packages']:
+ subdir = None
+ page_token = None
+ while True:
+ response = service.files().list(spaces="drive", pageToken=page_token, fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and trashed=false and name='" + str(section) + "' and '" + str(the_folder) + "' in parents").execute() # pylint: disable=no-member
+ for the_file in response.get('files', []):
+ if 'id' in the_file:
+ subdir = the_file['id']
+ break
+ page_token = response.get('nextPageToken', None)
+ if subdir is not None or page_token is None:
+ break
+ if subdir is None:
+ logmessage('trash_gd_project: section ' + str(section) + ' could not be found')
+ continue
+ subsubdir = None
+ page_token = None
+ while True:
+ response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and trashed=false and name='" + str(old_project) + "' and '" + str(subdir) + "' in parents").execute() # pylint: disable=no-member
+ for the_file in response.get('files', []):
+ if 'id' in the_file:
+ subsubdir = the_file['id']
+ break
+ page_token = response.get('nextPageToken', None)
+ if subsubdir is not None or page_token is None:
+ break
+ if subsubdir is None:
+ logmessage('trash_gd_project: project ' + str(old_project) + ' could not be found in ' + str(section))
+ continue
+ service.files().delete(fileId=subsubdir).execute() # pylint: disable=no-member
+ logmessage('trash_gd_project: project ' + str(old_project) + ' deleted in section ' + str(section))
+ return True
+
+
+def trash_gd_file(section, filename, current_project):
+ if section == 'template':
+ section = 'templates'
+ the_folder = get_gd_folder()
+ if the_folder is None:
+ logmessage('trash_gd_file: folder not configured')
+ return False
+ storage = RedisCredStorage(oauth_app='googledrive')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ logmessage('trash_gd_file: credentials missing or expired')
+ return False
+ http = credentials.authorize(httplib2.Http())
+ import apiclient # pylint: disable=import-outside-toplevel
+ service = apiclient.discovery.build('drive', 'v3', http=http)
+ response = service.files().get(fileId=the_folder, fields="mimeType, id, name, trashed").execute() # pylint: disable=no-member
+ trashed = response.get('trashed', False)
+ the_mime_type = response.get('mimeType', None)
+ if trashed is True or the_mime_type != "application/vnd.google-apps.folder":
+ logmessage('trash_gd_file: folder did not exist')
+ return False
+ subdir = None
+ response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and trashed=false and name='" + str(section) + "' and '" + str(the_folder) + "' in parents").execute() # pylint: disable=no-member
+ for the_file in response.get('files', []):
+ if 'id' in the_file:
+ subdir = the_file['id']
+ break
+ if subdir is None:
+ logmessage('trash_gd_file: section ' + str(section) + ' could not be found')
+ return False
+ if current_project != 'default':
+ response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and trashed=false and name='" + str(current_project) + "' and '" + str(subdir) + "' in parents").execute() # pylint: disable=no-member
+ subdir = None
+ for the_file in response.get('files', []):
+ if 'id' in the_file:
+ subdir = the_file['id']
+ break
+ if subdir is None:
+ logmessage('trash_gd_file: project ' + str(current_project) + ' could not be found')
+ return False
+ id_of_filename = None
+ response = service.files().list(spaces="drive", fields="nextPageToken, files(id, name)", q="mimeType!='application/vnd.google-apps.folder' and name='" + str(filename) + "' and '" + str(subdir) + "' in parents").execute() # pylint: disable=no-member
+ for the_file in response.get('files', []):
+ if 'id' in the_file:
+ id_of_filename = the_file['id']
+ break
+ if id_of_filename is None:
+ logmessage('trash_gd_file: file ' + str(filename) + ' could not be found in ' + str(section))
+ return False
+ service.files().delete(fileId=id_of_filename).execute() # pylint: disable=no-member
+ logmessage('trash_gd_file: file ' + str(filename) + ' permanently deleted from ' + str(section))
+ return True
+
+
+@develop_bp.route('/sync_with_google_drive', methods=['GET'])
+@login_required
+@roles_required(['admin', 'developer'])
+def sync_with_google_drive():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ current_project = get_current_project()
+ the_next = current_app.user_manager.make_safe_url_function(request.args.get('next', url_for('develop.playground_page', project=current_project)))
+ auto_next = request.args.get('auto_next', None)
+ if current_app.config['USE_GOOGLE_DRIVE'] is False:
+ flash(word("Google Drive is not configured"), "error")
+ return redirect(the_next)
+ storage = RedisCredStorage(oauth_app='googledrive')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ flow = get_gd_flow()
+ uri = flow.step1_get_authorize_url()
+ return redirect(uri)
+ task = celery_app.signature('tasks.sync_with_google_drive', args=[current_user.id]).delay()
+ session['taskwait'] = task.id
+ if auto_next:
+ return redirect(url_for('develop.gd_sync_wait', auto_next=auto_next))
+ return redirect(url_for('develop.gd_sync_wait', next=the_next))
+
+
+@develop_bp.route('/gdsyncing', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def gd_sync_wait():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ current_project = get_current_project()
+ next_url = current_app.user_manager.make_safe_url_function(request.args.get('next', url_for('develop.playground_page', project=current_project)))
+ auto_next_url = request.args.get('auto_next', None)
+ my_csrf = generate_csrf()
+ script = f"""
+ """
+ response = make_response(render_template('develop/gd_sync_wait.html', version_warning=None, bodyclass='daadminbody', extra_js=Markup(script), tab_title=word('Synchronizing'), page_title=word('Synchronizing'), next_page=next_url), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/onedrive_callback', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def onedrive_callback():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ for key in request.args:
+ logmessage("onedrive_callback: argument " + str(key) + ": " + str(request.args[key]))
+ if 'code' in request.args:
+ flow = get_od_flow()
+ credentials = flow.step2_exchange(request.args['code'])
+ storage = RedisCredStorage(oauth_app='onedrive')
+ storage.put(credentials)
+ error = None
+ elif 'error' in request.args:
+ error = request.args['error']
+ if 'error_description' in request.args:
+ error += '; ' + str(request.args['error_description'])
+ else:
+ error = word("could not connect to OneDrive")
+ if error:
+ flash(word('There was a OneDrive error: ' + error), 'error')
+ return redirect(url_for('user.profile'))
+ flash(word('Connected to OneDrive'), 'success')
+ return redirect(url_for('develope.onedrive_page'))
+
+
+def rename_od_project(old_project, new_project):
+ the_folder = get_od_folder()
+ if the_folder is None:
+ logmessage('rename_od_project: folder not configured')
+ return False
+ storage = RedisCredStorage(oauth_app='onedrive')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ logmessage('rename_od_project: credentials missing or expired')
+ return False
+ http = credentials.authorize(httplib2.Http())
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + urllibquote(the_folder), "GET")
+ if int(resp['status']) != 200:
+ trashed = True
+ else:
+ info = json.loads(content.decode())
+ # logmessage("Found " + repr(info))
+ trashed = bool(info.get('deleted', None))
+ if trashed is True or 'folder' not in info:
+ logmessage('rename_od_project: folder did not exist')
+ return False
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + urllibquote(the_folder) + "/children?$select=id,name,deleted,folder", "GET")
+ subdir = {}
+ for section in ['static', 'templates', 'questions', 'modules', 'sources', 'packages']:
+ subdir[section] = None
+ while True:
+ if int(resp['status']) != 200:
+ logmessage('rename_od_project: could not obtain subfolders')
+ return False
+ info = json.loads(content.decode())
+ for item in info.get('value', []):
+ if item.get('deleted', None) or 'folder' not in item:
+ continue
+ if item['name'] in subdir:
+ subdir[item['name']] = item['id']
+ if "@odata.nextLink" not in info:
+ break
+ resp, content = http.request(info["@odata.nextLink"], "GET")
+ for section, the_subdir in subdir.items():
+ if the_subdir is None:
+ logmessage('rename_od_project: could not obtain subfolder for ' + str(section))
+ continue
+ subsubdir = None
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(the_subdir) + "/children?$select=id,name,deleted,folder", "GET")
+ while True:
+ if int(resp['status']) != 200:
+ logmessage('rename_od_project: could not obtain contents of subfolder for ' + str(section))
+ break
+ info = json.loads(content.decode())
+ for item in info.get('value', []):
+ if item.get('deleted', None) or 'folder' not in item:
+ continue
+ if item['name'] == old_project:
+ subsubdir = item['id']
+ break
+ if subsubdir is not None or "@odata.nextLink" not in info:
+ break
+ resp, content = http.request(info["@odata.nextLink"], "GET")
+ if subsubdir is None:
+ logmessage("rename_od_project: subdirectory " + str(old_project) + " not found")
+ else:
+ headers = {'Content-Type': 'application/json'}
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(subsubdir), "PATCH", headers=headers, body=json.dumps({'name': new_project}))
+ if int(resp['status']) != 200:
+ logmessage('rename_od_project: could not rename folder ' + str(old_project) + " in " + str(section) + " because " + repr(content))
+ continue
+ logmessage('rename_od_project: project ' + str(old_project) + ' rename in section ' + str(section))
+ return True
+
+
+def trash_od_project(old_project):
+ the_folder = get_od_folder()
+ if the_folder is None:
+ logmessage('trash_od_project: folder not configured')
+ return False
+ storage = RedisCredStorage(oauth_app='onedrive')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ logmessage('trash_od_project: credentials missing or expired')
+ return False
+ http = credentials.authorize(httplib2.Http())
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + urllibquote(the_folder), "GET")
+ if int(resp['status']) != 200:
+ trashed = True
+ else:
+ info = json.loads(content.decode())
+ # logmessage("Found " + repr(info))
+ trashed = bool(info.get('deleted', None))
+ if trashed is True or 'folder' not in info:
+ logmessage('trash_od_project: folder did not exist')
+ return False
+ subdir = {}
+ for section in ['static', 'templates', 'questions', 'modules', 'sources', 'packages']:
+ subdir[section] = None
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + urllibquote(the_folder) + "/children?$select=id,name,deleted,folder", "GET")
+ while True:
+ if int(resp['status']) != 200:
+ logmessage('trash_od_project: could not obtain subfolders')
+ return False
+ info = json.loads(content.decode())
+ for item in info['value']:
+ if item.get('deleted', None) or 'folder' not in item:
+ continue
+ if item['name'] in subdir:
+ subdir[item['name']] = item['id']
+ if "@odata.nextLink" not in info:
+ break
+ resp, content = http.request(info["@odata.nextLink"], "GET")
+ for section, the_subdir in subdir.items():
+ if the_subdir is None:
+ logmessage('trash_od_project: could not obtain subfolder for ' + str(section))
+ continue
+ subsubdir = None
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(the_subdir) + "/children?$select=id,name,deleted,folder", "GET")
+ while True:
+ if int(resp['status']) != 200:
+ logmessage('trash_od_project: could not obtain contents of subfolder for ' + str(section))
+ break
+ info = json.loads(content.decode())
+ for item in info['value']:
+ if item.get('deleted', None) or 'folder' not in item:
+ continue
+ if item['name'] == old_project:
+ subsubdir = item['id']
+ break
+ if subsubdir is not None or "@odata.nextLink" not in info:
+ break
+ resp, content = http.request(info["@odata.nextLink"], "GET")
+ if subsubdir is None:
+ logmessage("Could not find subdirectory " + old_project + " in section " + str(section))
+ else:
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + urllibquote(subsubdir) + "/children?$select=id", "GET")
+ to_delete = []
+ while True:
+ if int(resp['status']) != 200:
+ logmessage('trash_od_project: could not obtain contents of project folder')
+ return False
+ info = json.loads(content.decode())
+ for item in info.get('value', []):
+ if 'id' in item:
+ to_delete.append(item['id'])
+ if "@odata.nextLink" not in info:
+ break
+ resp, content = http.request(info["@odata.nextLink"], "GET")
+ for item_id in to_delete:
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(item_id), "DELETE")
+ if int(resp['status']) != 204:
+ logmessage('trash_od_project: could not delete file ' + str(item_id) + ". Result: " + repr(content))
+ return False
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(subsubdir), "DELETE")
+ if int(resp['status']) != 204:
+ logmessage('trash_od_project: could not delete project ' + str(old_project) + ". Result: " + repr(content))
+ return False
+ logmessage('trash_od_project: project ' + str(old_project) + ' trashed in section ' + str(section))
+ return True
+
+
+def trash_od_file(section, filename, current_project):
+ if section == 'template':
+ section = 'templates'
+ the_folder = get_od_folder()
+ if the_folder is None:
+ logmessage('trash_od_file: folder not configured')
+ return False
+ storage = RedisCredStorage(oauth_app='onedrive')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ logmessage('trash_od_file: credentials missing or expired')
+ return False
+ http = credentials.authorize(httplib2.Http())
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + urllibquote(the_folder), "GET")
+ if int(resp['status']) != 200:
+ trashed = True
+ else:
+ info = json.loads(content.decode())
+ # logmessage("Found " + repr(info))
+ trashed = bool(info.get('deleted', None))
+ if trashed is True or 'folder' not in info:
+ logmessage('trash_od_file: folder did not exist')
+ return False
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + urllibquote(the_folder) + "/children?$select=id,name,deleted,folder", "GET")
+ subdir = None
+ while True:
+ if int(resp['status']) != 200:
+ logmessage('trash_od_file: could not obtain subfolders')
+ return False
+ info = json.loads(content.decode())
+ # logmessage("Found " + repr(info))
+ for item in info['value']:
+ if item.get('deleted', None) or 'folder' not in item:
+ continue
+ if item['name'] == section:
+ subdir = item['id']
+ break
+ if subdir is not None or "@odata.nextLink" not in info:
+ break
+ resp, content = http.request(info["@odata.nextLink"], "GET")
+ if subdir is None:
+ logmessage('trash_od_file: could not obtain subfolder')
+ return False
+ if current_project != 'default':
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(subdir) + "/children?$select=id,name,deleted,folder", "GET")
+ subdir = None
+ while True:
+ if int(resp['status']) != 200:
+ logmessage('trash_od_file: could not obtain subfolders to find project')
+ return False
+ info = json.loads(content.decode())
+ for item in info['value']:
+ if item.get('deleted', None) or 'folder' not in item:
+ continue
+ if item['name'] == current_project:
+ subdir = item['id']
+ break
+ if subdir is not None or "@odata.nextLink" not in info:
+ break
+ resp, content = http.request(info["@odata.nextLink"], "GET")
+ if subdir is None:
+ logmessage('trash_od_file: could not obtain subfolder')
+ return False
+ id_of_filename = None
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(subdir) + "/children?$select=id,name,deleted,folder", "GET")
+ while True:
+ if int(resp['status']) != 200:
+ logmessage('trash_od_file: could not obtain contents of subfolder')
+ return False
+ info = json.loads(content.decode())
+ # logmessage("Found " + repr(info))
+ for item in info['value']:
+ if item.get('deleted', None) or 'folder' in item:
+ continue
+ if 'folder' in item:
+ continue
+ if item['name'] == filename:
+ id_of_filename = item['id']
+ break
+ if id_of_filename is not None or "@odata.nextLink" not in info:
+ break
+ resp, content = http.request(info["@odata.nextLink"], "GET")
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(id_of_filename), "DELETE")
+ if int(resp['status']) != 204:
+ logmessage('trash_od_file: could not delete ')
+ return False
+ logmessage('trash_od_file: file ' + str(filename) + ' trashed from ' + str(section))
+ return True
+
+
+@develop_bp.route('/sync_with_onedrive', methods=['GET'])
+@login_required
+@roles_required(['admin', 'developer'])
+def sync_with_onedrive():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ # current_project = get_current_project()
+ the_next = current_app.user_manager.make_safe_url_function(request.args.get('next', url_for('develop.playground_page', project=get_current_project())))
+ auto_next = request.args.get('auto_next', None)
+ if current_app.config['USE_ONEDRIVE'] is False:
+ flash(word("OneDrive is not configured"), "error")
+ return redirect(the_next)
+ storage = RedisCredStorage(oauth_app='onedrive')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ flow = get_gd_flow()
+ uri = flow.step1_get_authorize_url()
+ return redirect(uri)
+ task = celery_app.signature('tasks.sync_with_onedrive', args=[current_user.id]).delay()
+ session['taskwait'] = task.id
+ if auto_next:
+ return redirect(url_for('develop.od_sync_wait', auto_next=auto_next))
+ return redirect(url_for('develop.od_sync_wait', next=the_next))
+
+
+@develop_bp.route('/odsyncing', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def od_sync_wait():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ current_project = get_current_project()
+ next_url = current_app.user_manager.make_safe_url_function(request.args.get('next', url_for('develop.playground_page', project=current_project)))
+ auto_next_url = request.args.get('auto_next', None)
+ if auto_next_url is not None:
+ auto_next_url = current_app.user_manager.make_safe_url_function(auto_next_url)
+ my_csrf = generate_csrf()
+ script = f"""
+ """
+ response = make_response(render_template('develop/od_sync_wait.html', version_warning=None, bodyclass='daadminbody', extra_js=Markup(script), tab_title=word('Synchronizing'), page_title=word('Synchronizing'), next_page=next_url), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+# @develop_bp.route('/old_sync_with_google_drive', methods=['GET', 'POST'])
+# @login_required
+# @roles_required(['admin', 'developer'])
+# def old_sync_with_google_drive():
+# next = request.args.get('next', url_for('develop.playground_page'))
+# extra_meta = """\n """
+# return render_template('develop/google_sync.html', version_warning=None, bodyclass='daadminbody', extra_meta=Markup(extra_meta), tab_title=word('Synchronizing'), page_title=word('Synchronizing'))
+
+
+def add_br(text):
+ return re.sub(r'[\n\r]+', "
", text)
+
+
+@develop_bp.route('/checkin_sync_with_google_drive', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def checkin_sync_with_google_drive():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ setup_translation()
+ if 'taskwait' not in session:
+ return jsonify(success=False)
+ result = celery_app.AsyncResult(id=session['taskwait'])
+ if result.ready():
+ if 'taskwait' in session:
+ del session['taskwait']
+ the_result = result.get()
+ if the_result.__class__.__name__ == 'ReturnValue':
+ if the_result.ok:
+ logmessage("checkin_sync_with_google_drive: success")
+ return jsonify(success=True, status='finished', ok=the_result.ok, summary=add_br(the_result.summary), restart=the_result.restart)
+ if hasattr(the_result, 'error'):
+ logmessage("checkin_sync_with_google_drive: failed return value is " + str(the_result.error))
+ return jsonify(success=True, status='failed', error_message=str(the_result.error), restart=False)
+ if hasattr(the_result, 'summary'):
+ return jsonify(success=True, status='failed', summary=add_br(the_result.summary), restart=False)
+ return jsonify(success=True, status='failed', error_message=str("No error message. Result is " + str(the_result)), restart=False)
+ logmessage("checkin_sync_with_google_drive: failed return value is a " + str(type(the_result)))
+ logmessage("checkin_sync_with_google_drive: failed return value is " + str(the_result))
+ return jsonify(success=True, status='failed', error_message=noquote(str(the_result)), restart=False)
+ return jsonify(success=True, status='waiting', restart=False)
+
+
+@develop_bp.route('/checkin_sync_with_onedrive', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def checkin_sync_with_onedrive():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ setup_translation()
+ if 'taskwait' not in session:
+ return jsonify(success=False)
+ result = celery_app.AsyncResult(id=session['taskwait'])
+ if result.ready():
+ if 'taskwait' in session:
+ del session['taskwait']
+ the_result = result.get()
+ if the_result.__class__.__name__ == 'ReturnValue':
+ if the_result.ok:
+ logmessage("checkin_sync_with_onedrive: success")
+ return jsonify(success=True, status='finished', ok=the_result.ok, summary=add_br(the_result.summary), restart=the_result.restart)
+ if hasattr(the_result, 'error'):
+ logmessage("checkin_sync_with_onedrive: failed return value is " + str(the_result.error))
+ return jsonify(success=True, status='failed', error_message=str(the_result.error), restart=False)
+ if hasattr(the_result, 'summary'):
+ return jsonify(success=True, status='failed', summary=add_br(the_result.summary), restart=False)
+ return jsonify(success=True, status='failed', error_message=str("No error message. Result is " + str(the_result)), restart=False)
+ logmessage("checkin_sync_with_onedrive: failed return value is a " + str(type(the_result)))
+ logmessage("checkin_sync_with_onedrive: failed return value is " + str(the_result))
+ return jsonify(success=True, status='failed', error_message=str(the_result), restart=False)
+ return jsonify(success=True, status='waiting', restart=False)
+
+
+@develop_bp.route('/google_drive', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def google_drive_page():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ if current_app.config['USE_GOOGLE_DRIVE'] is False:
+ flash(word("Google Drive is not configured"), "error")
+ return redirect(url_for('user.profile'))
+ form = GoogleDriveForm(request.form)
+ if request.method == 'POST' and form.cancel.data:
+ return redirect(url_for('user.profile'))
+ storage = RedisCredStorage(oauth_app='googledrive')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ flow = get_gd_flow()
+ uri = flow.step1_get_authorize_url()
+ # logmessage("google_drive_page: uri is " + str(uri))
+ return redirect(uri)
+ http = credentials.authorize(httplib2.Http())
+ import apiclient # pylint: disable=import-outside-toplevel
+ try:
+ service = apiclient.discovery.build('drive', 'v3', http=http)
+ except:
+ set_gd_folder(None)
+ storage.release_lock()
+ storage.locked_delete()
+ flow = get_gd_flow()
+ uri = flow.step1_get_authorize_url()
+ return redirect(uri)
+ items = [{'id': '', 'name': word('-- Do not link --')}]
+ # items = []
+ page_token = None
+ while True:
+ try:
+ response = service.files().list(spaces="drive", pageToken=page_token, fields="nextPageToken, files(id, name, mimeType, shortcutDetails)", q="trashed=false and 'root' in parents and (mimeType = 'application/vnd.google-apps.folder' or (mimeType = 'application/vnd.google-apps.shortcut' and shortcutDetails.targetMimeType = 'application/vnd.google-apps.folder'))").execute() # pylint: disable=no-member
+ except BaseException as err:
+ logmessage("google_drive_page: " + err.__class__.__name__ + ": " + str(err))
+ set_gd_folder(None)
+ storage.release_lock()
+ storage.locked_delete()
+ flash(word('There was a Google Drive error: ' + err.__class__.__name__ + ": " + str(err)), 'error')
+ return redirect(url_for('develop.google_drive_page'))
+ for the_file in response.get('files', []):
+ if the_file['mimeType'] == 'application/vnd.google-apps.shortcut':
+ the_file['id'] = the_file['shortcutDetails']['targetId']
+ items.append(the_file)
+ page_token = response.get('nextPageToken', None)
+ if page_token is None:
+ break
+ item_ids = [x['id'] for x in items if x['id'] != '']
+ if request.method == 'POST' and form.submit.data:
+ if form.folder.data == '':
+ set_gd_folder(None)
+ storage.locked_delete()
+ flash(word("Google Drive is not linked."), 'success')
+ elif form.folder.data in (-1, '-1'):
+ file_metadata = {
+ 'name': 'docassemble',
+ 'mimeType': 'application/vnd.google-apps.folder'
+ }
+ new_file = service.files().create(body=file_metadata, # pylint: disable=no-member
+ fields='id').execute()
+ new_folder = new_file.get('id', None)
+ set_gd_folder(new_folder)
+ gd_fix_subdirs(service, new_folder)
+ if new_folder is not None:
+ active_folder = {'id': new_folder, 'name': 'docassemble'}
+ items.append(active_folder)
+ item_ids.append(new_folder)
+ flash(word("Your Playground is connected to your Google Drive."), 'success')
+ elif form.folder.data in item_ids:
+ flash(word("Your Playground is connected to your Google Drive."), 'success')
+ set_gd_folder(form.folder.data)
+ gd_fix_subdirs(service, form.folder.data)
+ else:
+ flash(word("The supplied folder " + str(form.folder.data) + "could not be found."), 'error')
+ set_gd_folder(None)
+ return redirect(url_for('user.profile'))
+ the_folder = get_gd_folder()
+ active_folder = None
+ if the_folder is not None:
+ try:
+ response = service.files().get(fileId=the_folder, fields="mimeType, trashed").execute() # pylint: disable=no-member
+ except:
+ set_gd_folder(None)
+ return redirect(url_for('develop.google_drive_page'))
+ the_mime_type = response.get('mimeType', None)
+ trashed = response.get('trashed', False)
+ if trashed is False and the_mime_type == "application/vnd.google-apps.folder":
+ active_folder = {'id': the_folder, 'name': response.get('name', 'no name')}
+ if the_folder not in item_ids:
+ items.append(active_folder)
+ else:
+ set_gd_folder(None)
+ the_folder = None
+ flash(word("The mapping was reset because the folder does not appear to exist anymore."), 'error')
+ if the_folder is None:
+ for item in items:
+ if item['name'].lower() == 'docassemble':
+ active_folder = item
+ break
+ if active_folder is None:
+ active_folder = {'id': -1, 'name': 'docassemble'}
+ items.append(active_folder)
+ item_ids.append(-1)
+ if the_folder is not None:
+ gd_fix_subdirs(service, the_folder)
+ if the_folder is None:
+ the_folder = ''
+ description = 'Select the folder from your Google Drive that you want to be synchronized with the Playground.'
+ if current_app.config['USE_ONEDRIVE'] is True and get_od_folder() is not None:
+ description += ' ' + word('Note that if you connect to a Google Drive folder, you will disable your connection to OneDrive.')
+
+ response = make_response(render_template('develop/googledrive.html', version_warning=version_warning, description=description, bodyclass='daadminbody', header=word('Google Drive'), tab_title=word('Google Drive'), items=items, the_folder=the_folder, page_title=word('Google Drive'), form=form), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+def gd_fix_subdirs(service, the_folder):
+ subdirs = []
+ page_token = None
+ while True:
+ response = service.files().list(spaces="drive", pageToken=page_token, fields="nextPageToken, files(id, name)", q="mimeType='application/vnd.google-apps.folder' and trashed=false and '" + str(the_folder) + "' in parents").execute()
+ for the_file in response.get('files', []):
+ subdirs.append(the_file)
+ page_token = response.get('nextPageToken', None)
+ if page_token is None:
+ break
+ todo = set(['questions', 'static', 'sources', 'templates', 'modules', 'packages'])
+ done = set(x['name'] for x in subdirs if x['name'] in todo)
+ for key in todo - done:
+ file_metadata = {
+ 'name': key,
+ 'mimeType': 'application/vnd.google-apps.folder',
+ 'parents': [the_folder]
+ }
+ service.files().create(body=file_metadata,
+ fields='id').execute()
+
+
+@develop_bp.route('/onedrive', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def onedrive_page():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ if current_app.config['USE_ONEDRIVE'] is False:
+ flash(word("OneDrive is not configured"), "error")
+ return redirect(url_for('user.profile'))
+ form = OneDriveForm(request.form)
+ if request.method == 'POST' and form.cancel.data:
+ return redirect(url_for('user.profile'))
+ storage = RedisCredStorage(oauth_app='onedrive')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ flow = get_od_flow()
+ uri = flow.step1_get_authorize_url()
+ logmessage("one_drive_page: uri is " + str(uri))
+ return redirect(uri)
+ items = [{'id': '', 'name': word('-- Do not link --')}]
+ http = credentials.authorize(httplib2.Http())
+ try:
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/root/children?$select=id,name,deleted,folder", "GET")
+ except:
+ set_od_folder(None)
+ storage.release_lock()
+ storage.locked_delete()
+ flow = get_od_flow()
+ uri = flow.step1_get_authorize_url()
+ logmessage("one_drive_page: uri is " + str(uri))
+ return redirect(uri)
+ while True:
+ if int(resp['status']) != 200:
+ flash("Error: could not connect to OneDrive; response code was " + str(resp['status']) + ". " + content.decode(), 'danger')
+ return redirect(url_for('user.profile'))
+ info = json.loads(content.decode())
+ for item in info['value']:
+ if 'folder' not in item:
+ continue
+ items.append({'id': item['id'], 'name': item['name']})
+ if "@odata.nextLink" not in info:
+ break
+ resp, content = http.request(info["@odata.nextLink"], "GET")
+ item_ids = [x['id'] for x in items if x['id'] != '']
+ if request.method == 'POST' and form.submit.data:
+ if form.folder.data == '':
+ set_od_folder(None)
+ storage.locked_delete()
+ flash(word("OneDrive is not linked."), 'success')
+ elif form.folder.data in (-1, '-1'):
+ headers = {'Content-Type': 'application/json'}
+ info = {}
+ info['name'] = 'docassemble'
+ info['folder'] = {}
+ info["@microsoft.graph.conflictBehavior"] = "fail"
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/root/children", "POST", headers=headers, body=json.dumps(info))
+ if int(resp['status']) == 201:
+ new_item = json.loads(content.decode())
+ set_od_folder(new_item['id'])
+ od_fix_subdirs(http, new_item['id'])
+ flash(word("Your Playground is connected to your OneDrive."), 'success')
+ else:
+ flash(word("Could not create folder. " + content.decode()), 'danger')
+ elif form.folder.data in item_ids:
+ set_od_folder(form.folder.data)
+ od_fix_subdirs(http, form.folder.data)
+ flash(word("Your Playground is connected to your OneDrive."), 'success')
+ else:
+ flash(word("The supplied folder " + str(form.folder.data) + "could not be found."), 'danger')
+ set_od_folder(None)
+ return redirect(url_for('user.profile'))
+ the_folder = get_od_folder()
+ active_folder = None
+ if the_folder is not None:
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(the_folder), "GET")
+ if int(resp['status']) != 200:
+ set_od_folder(None)
+ flash(word("The previously selected OneDrive folder does not exist.") + " " + str(the_folder) + " " + str(content) + " status: " + repr(resp['status']), "info")
+ return redirect(url_for('develop.onedrive_page'))
+ info = json.loads(content.decode())
+ logmessage("Found " + repr(info))
+ if info.get('deleted', None):
+ set_od_folder(None)
+ flash(word("The previously selected OneDrive folder was deleted."), "info")
+ return redirect(url_for('develop.onedrive_page'))
+ active_folder = {'id': the_folder, 'name': info.get('name', 'no name')}
+ if the_folder not in item_ids:
+ items.append(active_folder)
+ item_ids.append(the_folder)
+ if the_folder is None:
+ for item in items:
+ if item['name'].lower() == 'docassemble':
+ active_folder = item
+ break
+ if active_folder is None:
+ active_folder = {'id': -1, 'name': 'docassemble'}
+ items.append(active_folder)
+ item_ids.append(-1)
+ if the_folder is not None:
+ od_fix_subdirs(http, the_folder)
+ if the_folder is None:
+ the_folder = ''
+ description = word('Select the folder from your OneDrive that you want to be synchronized with the Playground.')
+ if current_app.config['USE_GOOGLE_DRIVE'] is True and get_gd_folder() is not None:
+ description += ' ' + word('Note that if you connect to a OneDrive folder, you will disable your connection to Google Drive.')
+ response = make_response(render_template('develop/onedrive.html', version_warning=version_warning, bodyclass='daadminbody', header=word('OneDrive'), tab_title=word('OneDrive'), items=items, the_folder=the_folder, page_title=word('OneDrive'), form=form, description=Markup(description)), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+def od_fix_subdirs(http, the_folder):
+ subdirs = set()
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(the_folder) + "/children?$select=id,name,deleted,folder", "GET")
+ while True:
+ if int(resp['status']) != 200:
+ raise DAError("od_fix_subdirs: could not get contents of folder")
+ info = json.loads(content.decode())
+ logmessage("Found " + repr(info))
+ for item in info['value']:
+ if 'folder' in item:
+ subdirs.add(item['name'])
+ if "@odata.nextLink" not in info:
+ break
+ resp, content = http.request(info["@odata.nextLink"], "GET")
+ todo = set(['questions', 'static', 'sources', 'templates', 'modules', 'packages'])
+ for folder_name in (todo - subdirs):
+ headers = {'Content-Type': 'application/json'}
+ data = {}
+ data['name'] = folder_name
+ data['folder'] = {}
+ data["@microsoft.graph.conflictBehavior"] = "rename"
+ resp, content = http.request("https://graph.microsoft.com/v1.0/me/drive/items/" + str(the_folder) + "/children", "POST", headers=headers, body=json.dumps(data))
+ if int(resp['status']) != 201:
+ raise DAError("od_fix_subdirs: could not create subfolder " + folder_name + ' in ' + str(the_folder) + '. ' + content.decode() + ' status: ' + str(resp['status']))
+
+
+@develop_bp.route('/view_source', methods=['GET'])
+@login_required
+@roles_required(['developer', 'admin'])
+def view_source():
+ setup_translation()
+ source_path = request.args.get('i', None)
+ playground_user = get_playground_user()
+ current_project = get_current_project()
+ if source_path is None:
+ logmessage("view_source: no source path")
+ return ('File not found', 404)
+ try:
+ if re.search(r':', source_path):
+ source = interview_source_from_string(source_path)
+ else:
+ try:
+ source = interview_source_from_string('docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + source_path)
+ except:
+ source = interview_source_from_string(source_path)
+ except BaseException as errmess:
+ logmessage("view_source: no source: " + str(errmess))
+ return ('File not found', 404)
+ header = source_path
+ response = make_response(render_template('develop/view_source.html', version_warning=None, bodyclass='daadminbody', tab_title="Source", page_title="Source", extra_css=Markup('\n '), header=header, contents=Markup(highlight(source.content, YamlLexer(), HtmlFormatter(cssclass="highlight dahighlight dafullheight")))), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/playgroundstatic///', methods=['GET'])
+def playground_static(current_project, userid, filename):
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ # filename = re.sub(r'[^A-Za-z0-9\-\_\. ]', '', filename)
+ try:
+ attach = int(request.args.get('attach', 0))
+ except:
+ attach = 0
+ area = SavedFile(userid, fix=True, section='playgroundstatic')
+ the_directory = directory_for(area, current_project)
+ filename = filename.replace('/', os.path.sep)
+ path = os.path.join(the_directory, filename)
+ if os.path.join('..', '') in path:
+ return ('File not found', 404)
+ if os.path.isfile(path):
+ filename = os.path.basename(filename)
+ extension, mimetype = get_ext_and_mimetype(filename) # pylint: disable=unused-variable
+ response = custom_send_file(path, mimetype=str(mimetype), download_name=filename)
+ if attach:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(filename))
+ return response
+ return ('File not found', 404)
+
+
+@develop_bp.route('/playgroundmodules///', methods=['GET'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_modules(current_project, userid, filename):
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ setup_translation()
+ # filename = re.sub(r'[^A-Za-z0-9\-\_\. ]', '', filename)
+ try:
+ attach = int(request.args.get('attach', 0))
+ except:
+ attach = 0
+ area = SavedFile(userid, fix=True, section='playgroundmodules')
+ the_directory = directory_for(area, current_project)
+ filename = filename.replace('/', os.path.sep)
+ path = os.path.join(the_directory, filename)
+ if os.path.join('..', '') in path:
+ return ('File not found', 404)
+ if os.path.isfile(path):
+ filename = os.path.basename(filename)
+ extension, mimetype = get_ext_and_mimetype(filename) # pylint: disable=unused-variable
+ response = custom_send_file(path, mimetype=str(mimetype), download_name=filename)
+ if attach:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(filename))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ return ('File not found', 404)
+
+
+@develop_bp.route('/playgroundsources///', methods=['GET'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_sources(current_project, userid, filename):
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ setup_translation()
+ try:
+ attach = int(request.args.get('attach', 0))
+ except:
+ attach = 0
+ # filename = re.sub(r'[^A-Za-z0-9\-\_\(\)\. ]', '', filename)
+ filename = filename.replace('/', os.path.sep)
+ area = SavedFile(userid, fix=True, section='playgroundsources')
+ write_ml_source(area, userid, current_project, filename)
+ the_directory = directory_for(area, current_project)
+ path = os.path.join(the_directory, filename)
+ if os.path.join('..', '') in path:
+ return ('File not found', 404)
+ if os.path.isfile(path):
+ filename = os.path.basename(filename)
+ extension, mimetype = get_ext_and_mimetype(filename) # pylint: disable=unused-variable
+ response = custom_send_file(path, mimetype=str(mimetype), download_name=filename)
+ if attach:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(filename))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ return ('File not found', 404)
+
+
+@develop_bp.route('/playgroundtemplate///', methods=['GET'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_template(current_project, userid, filename):
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ # filename = re.sub(r'[^A-Za-z0-9\-\_\. ]', '', filename)
+ setup_translation()
+ try:
+ attach = int(request.args.get('attach', 0))
+ except:
+ attach = 0
+ area = SavedFile(userid, fix=True, section='playgroundtemplate')
+ the_directory = directory_for(area, current_project)
+ filename = filename.replace('/', os.path.sep)
+ path = os.path.join(the_directory, filename)
+ if os.path.join('..', '') in path:
+ return ('File not found', 404)
+ if os.path.isfile(path):
+ filename = os.path.basename(filename)
+ extension, mimetype = get_ext_and_mimetype(filename) # pylint: disable=unused-variable
+ response = custom_send_file(path, mimetype=str(mimetype), download_name=filename)
+ if attach:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(filename))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ return ('File not found', 404)
+
+
+@develop_bp.route('/playgrounddownload///', methods=['GET'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_download(current_project, userid, filename):
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ setup_translation()
+ # filename = re.sub(r'[^A-Za-z0-9\-\_\. ]', '', filename)
+ area = SavedFile(userid, fix=True, section='playground')
+ the_directory = directory_for(area, current_project)
+ filename = filename.replace('/', os.path.sep)
+ path = os.path.join(the_directory, filename)
+ if os.path.join('..', '') in path:
+ return ('File not found', 404)
+ if os.path.isfile(path):
+ filename = os.path.basename(filename)
+ extension, mimetype = get_ext_and_mimetype(path) # pylint: disable=unused-variable
+ response = custom_send_file(path, mimetype=str(mimetype))
+ response.headers['Content-type'] = 'text/plain; charset=utf-8'
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(filename))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ return ('File not found', 404)
+
+
+@develop_bp.route('/officefunctionfile', methods=['GET', 'POST'])
+@cross_origin(origins='*', methods=['GET', 'POST', 'HEAD'], automatic_options=True)
+def playground_office_functionfile():
+ g.embed = True
+ set_language(DEFAULT_LANGUAGE)
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ functionform = FunctionFileForm(request.form)
+ response = make_response(render_template('develop/officefunctionfile.html', current_project=get_current_project(), page_title=word("Docassemble Playground"), tab_title=word("Playground"), parent_origin=daconfig.get('office addin url', daconfig.get('url root', get_base_url())), form=functionform), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/officetaskpane', methods=['GET', 'POST'])
+@cross_origin(origins='*', methods=['GET', 'POST', 'HEAD'], automatic_options=True)
+def playground_office_taskpane():
+ g.embed = True
+ set_language(DEFAULT_LANGUAGE)
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ default_da_server = url_for('interview.rootindex', _external=True)
+ response = make_response(render_template('develop/officeouter.html', page_title=word("Docassemble Playground"), tab_title=word("Playground"), default_da_server=default_da_server, extra_js=Markup(f"\n ")), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/officeaddin', methods=['GET', 'POST'])
+@cross_origin(origins='*', methods=['GET', 'POST', 'HEAD'], automatic_options=True)
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_office_addin():
+ g.embed = True
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ playground_user = get_playground_user()
+ project_to_use = werkzeug.utils.secure_filename(request.args.get('project', get_current_project()))
+ if request.args.get('fetchfiles', None):
+ playground = SavedFile(playground_user.id, fix=True, section='playground')
+ the_directory = directory_for(playground, project_to_use)
+ if not os.path.isdir(the_directory):
+ return ('File not found', 404)
+ files = sorted([f for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9]', f)])
+ return jsonify(success=True, files=files, projects=get_list_of_projects(playground_user.id))
+ pg_var_file = request.args.get('pgvars', None)
+ # logmessage("playground_office_addin: YAML file is " + str(pg_var_file))
+ use_html = request.args.get('html', False)
+ uploadform = AddinUploadForm(request.form)
+ if request.method == 'POST':
+ area = SavedFile(playground_user.id, fix=True, section='playgroundtemplate')
+ filename = secure_filename(uploadform.filename.data)
+ filename = re.sub(r'[^A-Za-z0-9\-\_\. ]+', '_', filename)
+ if filename == '':
+ return jsonify({'success': False})
+ content = str(uploadform.content.data)
+ start_index = 0
+ char_index = 0
+ for char in content:
+ char_index += 1
+ if char == ',':
+ start_index = char_index
+ break
+ area.write_content(codecs.decode(bytearray(content[start_index:], encoding='utf-8'), 'base64'), filename=filename, binary=True, project=project_to_use)
+ area.finalize()
+ if use_html:
+ if pg_var_file is None:
+ pg_var_file = ''
+ else:
+ if pg_var_file is None or pg_var_file == '':
+ return jsonify({'success': True, 'variables_json': [], 'vocab_list': []})
+ if pg_var_file is not None:
+ playground = SavedFile(playground_user.id, fix=True, section='playground')
+ the_directory = directory_for(playground, project_to_use)
+ files = sorted([f for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9]', f)])
+ if pg_var_file in files:
+ # logmessage("playground_office_addin: file " + str(pg_var_file) + " was found")
+ interview_source = interview_source_from_string('docassemble.playground' + str(playground_user.id) + project_name(project_to_use) + ':' + pg_var_file, raise_jinja_errors=False)
+ interview_source.set_testing(True)
+ else:
+ # logmessage("playground_office_addin: file " + str(pg_var_file) + " was not found")
+ if pg_var_file == '' and project_to_use == 'default':
+ pg_var_file = 'test.yml'
+ content = "modules:\n - docassemble.base.util\n---\nmandatory: True\nquestion: hi"
+ interview_source = InterviewSourceString(content=content, directory=the_directory, package="docassemble.playground" + str(playground_user.id) + project_name(project_to_use), path="docassemble.playground" + str(playground_user.id) + project_name(project_to_use) + ":" + pg_var_file, testing=True)
+ interview = Interview(source=interview_source)
+ ensure_ml_file_exists(interview, pg_var_file, project_to_use)
+ the_current_info = current_info(yaml='docassemble.playground' + str(playground_user.id) + project_name(project_to_use) + ':' + pg_var_file, req=request, action=None, device_id=request.cookies.get('ds', None))
+ this_thread.current_info = the_current_info
+ interview_status = InterviewStatus(current_info=the_current_info)
+ if use_html:
+ variables_html, vocab_list, vocab_dict, ac_list = get_vars_in_use(interview, interview_status, debug_mode=False, show_messages=False, show_jinja_help=True, current_project=project_to_use) # pylint: disable=unused-variable
+ return jsonify({'success': True, 'current_project': project_to_use, 'variables_html': variables_html, 'vocab_list': list(vocab_list), 'vocab_dict': vocab_dict})
+ variables_json, vocab_list, vocab_dict, ac_list = get_vars_in_use(interview, interview_status, debug_mode=False, return_json=True, current_project=project_to_use)
+ return jsonify({'success': True, 'variables_json': variables_json, 'vocab_list': list(vocab_list)})
+ parent_origin = re.sub(r'^(https?://[^/]+)/.*', r'\1', daconfig.get('office addin url', get_base_url()))
+ response = make_response(render_template('develop/officeaddin.html', current_project=project_to_use, page_title=word("Docassemble Office Add-in"), tab_title=word("Office Add-in"), parent_origin=parent_origin, form=uploadform), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+def cloud_trash(use_gd, use_od, section, the_file, current_project):
+ if use_gd:
+ try:
+ trash_gd_file(section, the_file, current_project)
+ except BaseException as the_err:
+ logmessage("cloud_trash: unable to delete file on Google Drive. " + str(the_err))
+ elif use_od:
+ try:
+ trash_od_file(section, the_file, current_project)
+ except BaseException as the_err:
+ try:
+ logmessage("cloud_trash: unable to delete file on OneDrive. " + str(the_err))
+ except:
+ logmessage("cloud_trash: unable to delete file on OneDrive.")
+
+
+@develop_bp.route('/playgroundfiles', methods=['GET', 'POST'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_files():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ setup_translation()
+ playground_user = get_playground_user()
+ current_project = get_current_project()
+ use_gd = bool(current_app.config['USE_GOOGLE_DRIVE'] is True and get_gd_folder() is not None)
+ use_od = bool(use_gd is False and current_app.config['USE_ONEDRIVE'] is True and get_od_folder() is not None)
+ form = PlaygroundFilesForm(request.form)
+ formtwo = PlaygroundFilesEditForm(request.form)
+ is_ajax = bool('ajax' in request.form and int(request.form['ajax']))
+ section = werkzeug.utils.secure_filename(request.args.get('section', 'template'))
+ the_file = secure_filename_spaces_ok(request.args.get('file', ''))
+ scroll = False
+ if the_file != '':
+ scroll = True
+ if request.method == 'GET':
+ is_new = true_or_false(request.args.get('new', False))
+ else:
+ is_new = False
+ if is_new:
+ scroll = True
+ the_file = ''
+ if request.method == 'POST':
+ form_validated = bool((form.purpose.data == 'upload' and form.validate()) or (formtwo.purpose.data == 'edit' and formtwo.validate()))
+ if form_validated:
+ if form.section.data:
+ section = form.section.data
+ if formtwo.file_name.data:
+ the_file = formtwo.file_name.data
+ the_file = re.sub(r'[^A-Za-z0-9\-\_\. ]+', '_', the_file)
+ else:
+ form_validated = None
+ if section not in ("template", "static", "sources", "modules", "packages"):
+ section = "template"
+ pgarea = SavedFile(playground_user.id, fix=True, section='playground')
+ the_directory = directory_for(pgarea, current_project)
+ if current_project != 'default' and not os.path.isdir(the_directory):
+ current_project = set_current_project('default')
+ the_directory = directory_for(pgarea, current_project)
+ dropdown_files = sorted([f for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9]', f)])
+ current_variable_file = get_variable_file(current_project)
+ if current_variable_file is not None:
+ if current_variable_file in dropdown_files:
+ active_file = current_variable_file
+ else:
+ delete_variable_file(current_project)
+ active_file = None
+ else:
+ active_file = None
+ if active_file is None:
+ current_file = get_current_file(current_project, 'questions')
+ if current_file in dropdown_files:
+ active_file = current_file
+ elif len(dropdown_files) > 0:
+ delete_current_file(current_project, 'questions')
+ active_file = dropdown_files[0]
+ else:
+ delete_current_file(current_project, 'questions')
+ area = SavedFile(playground_user.id, fix=True, section='playground' + section)
+ the_directory = directory_for(area, current_project)
+ if request.args.get('delete', False):
+ # argument = re.sub(r'[^A-Za-z0-9\-\_\. ]', '', request.args.get('delete'))
+ argument = request.args.get('delete')
+ if argument:
+ the_directory = directory_for(area, current_project)
+ the_file = add_project(argument, current_project)
+ filename = os.path.join(the_directory, argument)
+ if os.path.exists(filename):
+ os.remove(filename)
+ area.finalize()
+ for key in r.keys('da:interviewsource:docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':*'):
+ r.incr(key.decode())
+ cloud_trash(use_gd, use_od, section, argument, current_project)
+ flash(word("Deleted file: ") + the_file, "success")
+ for key in r.keys('da:interviewsource:docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':*'):
+ r.incr(key.decode())
+ return redirect(url_for('develop.playground_files', section=section, project=current_project))
+ flash(word("File not found: ") + argument, "error")
+ if request.args.get('convert', False):
+ # argument = re.sub(r'[^A-Za-z0-9\-\_\. ]', '', request.args.get('convert'))
+ argument = request.args.get('convert')
+ if argument:
+ filename = os.path.join(the_directory, argument)
+ if os.path.exists(filename):
+ to_file = os.path.splitext(argument)[0] + '.md'
+ to_path = os.path.join(the_directory, to_file)
+ if not os.path.exists(to_path):
+ extension, mimetype = get_ext_and_mimetype(argument)
+ if mimetype and mimetype in convertible_mimetypes:
+ the_format = convertible_mimetypes[mimetype]
+ elif extension and extension in convertible_extensions:
+ the_format = convertible_extensions[extension]
+ else:
+ flash(word("File format not understood: ") + argument, "error")
+ return redirect(url_for('develop.playground_files', section=section, project=current_project))
+ if CAN_CONVERT_WORD:
+ result = word_to_markdown(filename, the_format)
+ else:
+ result = None
+ if result is None:
+ flash(word("File could not be converted: ") + argument, "error")
+ return redirect(url_for('develop.playground_files', section=section, project=current_project))
+ shutil.copyfile(result.name, to_path)
+ flash(word("Created new Markdown file called ") + to_file + word("."), "success")
+ area.finalize()
+ return redirect(url_for('develop.playground_files', section=section, file=to_file, project=current_project))
+ else:
+ flash(word("File not found: ") + argument, "error")
+ if request.method == 'POST' and form_validated:
+ if 'uploadfile' in request.files:
+ the_files = request.files.getlist('uploadfile')
+ if the_files:
+ need_to_restart = False
+ for up_file in the_files:
+ try:
+ filename = werkzeug.utils.secure_filename(up_file.filename)
+ extension, mimetype = get_ext_and_mimetype(filename) # pylint: disable=unused-variable
+ if section == 'modules' and extension != 'py':
+ flash(word("Sorry, only .py files can be uploaded here. To upload other types of files, use other Folders."), 'error')
+ return redirect(url_for('develop.playground_files', section=section, project=current_project))
+ filename = re.sub(r'[^A-Za-z0-9\-\_\. ]+', '_', filename)
+ the_file = filename
+ filename = os.path.join(the_directory, filename)
+ up_file.save(filename)
+ for key in r.keys('da:interviewsource:docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':*'):
+ r.incr(key.decode())
+ area.finalize()
+ if section == 'modules':
+ need_to_restart = True
+ except BaseException as err_mess:
+ flash("Error of type " + str(type(err_mess)) + " processing upload: " + str(err_mess), "error")
+ if need_to_restart:
+ flash(word('Since you uploaded a Python module, the server needs to restart in order to load your module.'), 'info')
+ return redirect(url_for('main.restart_page', next=url_for('develop.playground_files', section=section, file=the_file, project=current_project)))
+ flash(word("Upload successful"), "success")
+ if formtwo.delete.data:
+ if the_file != '':
+ filename = os.path.join(the_directory, the_file)
+ if os.path.exists(filename):
+ os.remove(filename)
+ for key in r.keys('da:interviewsource:docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':*'):
+ r.incr(key.decode())
+ area.finalize()
+ flash(word("Deleted file: ") + the_file, "success")
+ return redirect(url_for('develop.playground_files', section=section, project=current_project))
+ flash(word("File not found: ") + the_file, "error")
+ if formtwo.submit.data and formtwo.file_content.data:
+ if the_file != '':
+ if section == 'modules' and not re.search(r'\.py$', the_file):
+ the_file = re.sub(r'\..*', '', the_file) + '.py'
+ if formtwo.original_file_name.data and formtwo.original_file_name.data != the_file:
+ old_filename = os.path.join(the_directory, formtwo.original_file_name.data)
+ cloud_trash(use_gd, use_od, section, formtwo.original_file_name.data, current_project)
+ if os.path.isfile(old_filename):
+ os.remove(old_filename)
+ filename = os.path.join(the_directory, the_file)
+ with open(filename, 'w', encoding='utf-8') as fp:
+ fp.write(re.sub(r'\r\n', r'\n', formtwo.file_content.data))
+ the_time = formatted_current_time()
+ for key in r.keys('da:interviewsource:docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':*'):
+ r.incr(key.decode())
+ area.finalize()
+ if formtwo.active_file.data and formtwo.active_file.data != the_file:
+ # interview_file = os.path.join(pgarea.directory, formtwo.active_file.data)
+ r.incr('da:interviewsource:docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + formtwo.active_file.data)
+ # if os.path.isfile(interview_file):
+ # with open(interview_file, 'a'):
+ # os.utime(interview_file, None)
+ # pgarea.finalize()
+ flash_message = flash_as_html(str(the_file) + ' ' + word('was saved at') + ' ' + the_time + '.', message_type='success', is_ajax=is_ajax)
+ if section == 'modules':
+ # restart_all()
+ flash(word('Since you changed a Python module, the server needs to restart in order to load your module.'), 'info')
+ return redirect(url_for('main.restart_page', next=url_for('develop.playground_files', section=section, file=the_file, project=current_project)))
+ if is_ajax:
+ return jsonify(success=True, flash_message=flash_message)
+ return redirect(url_for('develop.playground_files', section=section, file=the_file, project=current_project))
+ flash(word('You need to type in a name for the file'), 'error')
+ if is_ajax and not form_validated:
+ errors = []
+ for field_name, error_messages in formtwo.errors.items():
+ for err in error_messages:
+ errors.append({'fieldName': field_name, 'err': err})
+ return jsonify(success=False, errors=errors)
+ files = sorted([f for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9]', f)])
+
+ editable_files = []
+ convertible_files = []
+ trainable_files = {}
+ mode = "yml"
+ for a_file in files:
+ extension, mimetype = get_ext_and_mimetype(a_file)
+ if (mimetype and mimetype in ok_mimetypes) or (extension and extension in ok_extensions) or (mimetype and mimetype.startswith('text')):
+ if section == 'sources' and re.match(r'ml-.*\.json$', a_file):
+ trainable_files[a_file] = re.sub(r'^ml-|\.json$', '', a_file)
+ else:
+ editable_files.append({'name': a_file, 'modtime': os.path.getmtime(os.path.join(the_directory, a_file))})
+ assign_opacity(editable_files)
+ editable_file_listing = [x['name'] for x in editable_files]
+ if CAN_CONVERT_WORD:
+ for a_file in files:
+ extension, mimetype = get_ext_and_mimetype(a_file)
+ b_file = os.path.splitext(a_file)[0] + '.md'
+ if b_file not in editable_file_listing and ((mimetype and mimetype in convertible_mimetypes) or (extension and extension in convertible_extensions)):
+ convertible_files.append(a_file)
+ if the_file and not is_new and the_file not in editable_file_listing:
+ the_file = ''
+ if not the_file and not is_new:
+ current_file = get_current_file(current_project, section)
+ if current_file in editable_file_listing:
+ the_file = current_file
+ else:
+ delete_current_file(current_project, section)
+ if len(editable_files) > 0:
+ the_file = sorted(editable_files, key=lambda x: x['modtime'])[-1]['name']
+ else:
+ if section == 'modules':
+ the_file = 'test.py'
+ elif section == 'sources':
+ the_file = 'test.json'
+ else:
+ the_file = 'test.md'
+ if the_file in editable_file_listing:
+ set_current_file(current_project, section, the_file)
+ if the_file != '':
+ mode, mimetype = get_ext_and_mimetype(the_file)
+ if mode != 'md':
+ active_file = None
+ if section == 'modules':
+ mode = 'py'
+ formtwo.original_file_name.data = the_file
+ formtwo.file_name.data = the_file
+ if the_file != '' and os.path.isfile(os.path.join(the_directory, the_file)):
+ filename = os.path.join(the_directory, the_file)
+ else:
+ filename = None
+ if filename is not None:
+ area.finalize()
+ with open(filename, 'r', encoding='utf-8') as fp:
+ try:
+ content = fp.read()
+ except:
+ filename = None
+ content = ''
+ elif formtwo.file_content.data:
+ content = re.sub(r'\r\n', r'\n', formtwo.file_content.data)
+ else:
+ content = ''
+ lowerdescription = None
+ description = None
+ if section == "template":
+ header = word("Templates")
+ description = 'Add files here that you want want to include in your interviews using docx template file, pdf template file, content file, initial yaml, additional yaml, template file, rtf template file, or docx reference file.'
+ upload_header = word("Upload a template file")
+ list_header = word("Existing template files")
+ edit_header = word('Edit text files')
+ after_text = None
+ elif section == "static":
+ header = word("Static Files")
+ description = 'Add files here that you want to include in your interviews with images, image sets, [FILE] or url_of().'
+ upload_header = word("Upload a static file")
+ list_header = word("Existing static files")
+ edit_header = word('Edit text files')
+ after_text = None
+ elif section == "sources":
+ header = word("Source Files")
+ description = 'Add files here that you want to use as a data source in your interview code, such as word translation files and training data for machine learning. For Python source code, see the Modules folder.'
+ upload_header = word("Upload a source file")
+ list_header = word("Existing source files")
+ edit_header = word('Edit source files')
+ after_text = None
+ else: # section == "modules":
+ header = word("Modules")
+ upload_header = word("Upload a Python module")
+ list_header = word("Existing module files")
+ edit_header = word('Edit module files')
+ description = 'You can use this page to add Python module files (.py files) that you want to include in your interviews using modules or imports.'
+ lowerdescription = Markup("""To use this in an interview, write a modules block that refers to this module using Python's syntax for specifying a "relative import" of a module (i.e., prefix the module name with a period).
""" + highlight('---\nmodules:\n - .' + re.sub(r'\.py$', '', the_file) + '\n---', YamlLexer(), HtmlFormatter(cssclass='highlight dahighlight')) + """If you wish to refer to this module from another package, you can use a fully qualified reference.
""" + highlight('---\nmodules:\n - ' + "docassemble.playground" + str(playground_user.id) + project_name(current_project) + "." + re.sub(r'\.py$', '', the_file) + '\n---', YamlLexer(), HtmlFormatter(cssclass='highlight dahighlight')))
+ after_text = None
+ initial_values = playground_values(current_project, the_file, playground_user)
+ initial_values.update({
+ "daPage": 'files',
+ "daScroll": bool(scroll),
+ "isNew": bool(is_new),
+ "existingFiles": files,
+ "daSection": section,
+ "daUrlPlaygroundFiles": url_for('develop.playground_files', project=current_project),
+ "daContent": content,
+ "daMode": mode
+ })
+ extra_js = f"""
+
+ {redis_script(initial_values)}
+"""
+ any_files = bool(len(editable_files) > 0)
+ back_button = Markup('')
+ if current_user.id != playground_user.id:
+ header += " / " + playground_user.email
+ if current_project != 'default':
+ header += " / " + current_project
+ response = make_response(render_template('develop/playgroundfiles.html', current_project=current_project, version_warning=None, bodyclass='daadminbody', use_gd=use_gd, use_od=use_od, back_button=back_button, tab_title=header, page_title=header, extra_css=Markup('\n '), extra_js=Markup(extra_js), header=header, upload_header=upload_header, list_header=list_header, edit_header=edit_header, description=Markup(description), lowerdescription=lowerdescription, form=form, files=sorted(files, key=lambda y: y.lower()), section=section, userid=playground_user.id, editable_files=sorted(editable_files, key=lambda y: y['name'].lower()), editable_file_listing=editable_file_listing, trainable_files=trainable_files, convertible_files=convertible_files, formtwo=formtwo, current_file=the_file, content=content, after_text=after_text, is_new=str(is_new), any_files=any_files, dropdown_files=sorted(dropdown_files, key=lambda y: y.lower()), active_file=active_file, playground_package='docassemble.playground' + str(playground_user.id) + project_name(current_project), own_playground=bool(playground_user.id == current_user.id)), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/pullplaygroundpackage', methods=['GET', 'POST'])
+@login_required
+@roles_required(['developer', 'admin'])
+def pull_playground_package():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ current_project = get_current_project()
+ form = PullPlaygroundPackage(request.form)
+ if request.method == 'POST':
+ if form.pull.data:
+ if form.github_url.data and form.pypi.data:
+ flash(word("You cannot pull from GitHub and PyPI at the same time. Please fill in one and leave the other blank."), 'error')
+ elif form.github_url.data:
+ return redirect(url_for('develop.playground_packages', project=current_project, pull='1', github_url=re.sub(r'/*$', '', str(form.github_url.data).strip()), branch=form.github_branch.data))
+ elif form.pypi.data:
+ return redirect(url_for('develop.playground_packages', project=current_project, pull='1', pypi=form.pypi.data))
+ if form.cancel.data:
+ return redirect(url_for('develop.playground_packages', project=current_project))
+ elif 'github' in request.args:
+ form.github_url.data = re.sub(r'[^A-Za-z0-9\-\.\_\~\:\/\?\#\[\]\@\!\$\&\'\(\)\*\+\,\;\=\`]', '', request.args['github'])
+ elif 'pypi' in request.args:
+ form.pypi.data = re.sub(r'[^A-Za-z0-9\-\.\_\~\:\/\?\#\[\]\@\!\$\&\'\(\)\*\+\,\;\=\`]', '', request.args['pypi'])
+ form.github_branch.choices = []
+ description = word("Enter a URL of a GitHub repository containing an extension package. When you press Pull, the contents of that repository will be copied into the Playground, overwriting any files with the same names. Or, put in the name of a PyPI package and it will do the same with the package on PyPI.")
+ branch = request.args.get('branch')
+ initial_values = {
+ "daDefaultBranch": branch if branch else GITHUB_BRANCH,
+ "daGetGitBranches": url_for('develop.get_git_branches'),
+ "daGithubBranch": GITHUB_BRANCH
+ }
+ extra_js = f"""
+
+
+"""
+ response = make_response(render_template('develop/pull_playground_package.html',
+ current_project=current_project,
+ form=form,
+ description=description,
+ version_warning=version_warning,
+ bodyclass='daadminbody',
+ title=word("Pull GitHub or PyPI Package"),
+ tab_title=word("Pull"),
+ page_title=word("Pull"),
+ extra_js=Markup(extra_js)), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+def get_branches_of_repo(giturl):
+ repo_name = re.sub(r'/*$', '', giturl)
+ m = re.search(r'//(.+):x-oauth-basic@github.com', repo_name)
+ if m:
+ access_token = m.group(1)
+ else:
+ access_token = None
+ repo_name = re.sub(r'^git\+', '', repo_name)
+ repo_name = re.sub(r'^http.*github.com/', '', repo_name)
+ repo_name = re.sub(r'.*@github.com:', '', repo_name)
+ repo_name = re.sub(r'[@#].*', '', repo_name)
+ repo_name = re.sub(r'.git$', '', repo_name)
+ if current_app.config['USE_GITHUB']:
+ github_auth = r.get('da:using_github:userid:' + str(current_user.id))
+ else:
+ github_auth = None
+ if github_auth and access_token is None:
+ storage = RedisCredStorage(oauth_app='github')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ http = httplib2.Http()
+ else:
+ http = credentials.authorize(httplib2.Http())
+ else:
+ http = httplib2.Http()
+ the_url = "https://api.github.com/repos/" + repo_name + '/branches'
+ branches = []
+ if access_token:
+ resp, content = http.request(the_url, "GET", headers={'Authorization': "token " + access_token})
+ else:
+ resp, content = http.request(the_url, "GET")
+ if int(resp['status']) == 200:
+ branches.extend(json.loads(content.decode()))
+ while True:
+ next_link = get_next_link(resp)
+ if next_link:
+ if access_token:
+ resp, content = http.request(next_link, "GET", headers={'Authorization': "token " + access_token})
+ else:
+ resp, content = http.request(next_link, "GET")
+ if int(resp['status']) != 200:
+ raise DAException(repo_name + " fetch failed")
+ branches.extend(json.loads(content.decode()))
+ else:
+ break
+ return branches
+ raise DAException(the_url + " fetch failed on first try; got " + str(resp['status']))
+
+
+@develop_bp.route('/get_git_branches', methods=['GET'])
+@login_required
+@roles_required(['developer', 'admin'])
+def get_git_branches():
+ if 'url' not in request.args:
+ return ('File not found', 404)
+ giturl = request.args['url'].strip()
+ try:
+ return jsonify({'success': True, 'result': get_branches_of_repo(giturl)})
+ except BaseException as err:
+ return jsonify({'success': False, 'reason': str(err)})
+
+
+def get_user_repositories(http):
+ repositories = []
+ resp, content = http.request("https://api.github.com/user/repos", "GET")
+ if int(resp['status']) == 200:
+ repositories.extend(json.loads(content.decode()))
+ while True:
+ next_link = get_next_link(resp)
+ if next_link:
+ resp, content = http.request(next_link, "GET")
+ if int(resp['status']) != 200:
+ raise DAError("get_user_repositories: could not get information from next URL")
+ repositories.extend(json.loads(content.decode()))
+ else:
+ break
+ else:
+ raise DAError("playground_packages: could not get information about repositories")
+ return repositories
+
+
+def get_orgs_info(http):
+ orgs_info = []
+ resp, content = http.request("https://api.github.com/user/orgs", "GET")
+ if int(resp['status']) == 200:
+ orgs_info.extend(json.loads(content.decode()))
+ while True:
+ next_link = get_next_link(resp)
+ if next_link:
+ resp, content = http.request(next_link, "GET")
+ if int(resp['status']) != 200:
+ raise DAError("get_orgs_info: could not get additional information about organizations")
+ orgs_info.extend(json.loads(content.decode()))
+ else:
+ break
+ else:
+ raise DAError("get_orgs_info: failed to get orgs using https://api.github.com/user/orgs")
+ return orgs_info
+
+
+def get_branch_info(http, full_name):
+ branch_info = []
+ resp, content = http.request("https://api.github.com/repos/" + str(full_name) + '/branches', "GET")
+ if int(resp['status']) == 200:
+ branch_info.extend(json.loads(content.decode()))
+ while True:
+ next_link = get_next_link(resp)
+ if next_link:
+ resp, content = http.request(next_link, "GET")
+ if int(resp['status']) != 200:
+ raise DAError("get_branch_info: could not get additional information from next URL")
+ branch_info.extend(json.loads(content))
+ else:
+ break
+ else:
+ logmessage("get_branch_info: could not get info from https://api.github.com/repos/" + str(full_name) + '/branches')
+ return branch_info
+
+
+def fix_package_folder():
+ playground_user = get_playground_user()
+ use_gd = bool(current_app.config['USE_GOOGLE_DRIVE'] is True and get_gd_folder() is not None)
+ use_od = bool(use_gd is False and current_app.config['USE_ONEDRIVE'] is True and get_od_folder() is not None)
+ problem_exists = False
+ area = SavedFile(playground_user.id, fix=True, section='playgroundpackages')
+ for f in os.listdir(area.directory):
+ path = os.path.join(area.directory, f)
+ if os.path.isfile(path) and not f.startswith('docassemble.') and not f.startswith('.'):
+ os.rename(path, os.path.join(area.directory, 'docassemble.' + f))
+ cloud_trash(use_gd, use_od, 'packages', f, 'default')
+ problem_exists = True
+ if os.path.isdir(path) and not f.startswith('.'):
+ for e in os.listdir(path):
+ if os.path.isfile(os.path.join(path, e)) and not e.startswith('docassemble.') and not e.startswith('.'):
+ os.rename(os.path.join(path, e), os.path.join(path, 'docassemble.' + e))
+ cloud_trash(use_gd, use_od, 'packages', e, f)
+ problem_exists = True
+ if problem_exists:
+ area.finalize()
+
+
+def secure_git_branchname(branch):
+ """Makes an input branch name a valid git branch name, and also strips out
+ things that would interpolated in bash."""
+ # The rules of what's allowed in a git branch name are: https://git-scm.com/docs/git-check-ref-format
+ branch = unicodedata.normalize("NFKD", branch)
+ branch = branch.encode("ascii", "ignore").decode("ascii")
+ branch = re.compile(r"[\u0000-\u0020]|(\")|(@{)|(\.\.)|[\u0170~ ^:?*$`[\\]|(//+)").sub("", branch)
+ branch = branch.strip("/")
+ # Can include a slash, but no slash-separated component can begin with a dot `.` or end with `.lock`
+ branch = "/".join([re.compile(r"\.lock$").sub("", component.lstrip('.')) for component in branch.split("/")])
+ branch = branch.rstrip(".")
+ if branch == "@":
+ branch = "_"
+ return branch
+
+
+def do_playground_pull(area, current_project, github_url=None, branch=None, pypi_package=None, can_publish_to_github=False, github_email=None, pull_only=False):
+ playground_user = get_playground_user()
+ area_sec = {'templates': 'playgroundtemplate', 'static': 'playgroundstatic', 'sources': 'playgroundsources', 'questions': 'playground'}
+ readme_text = ''
+ gitignore_text = ''
+ setup_py = ''
+ pyproject_toml = ''
+ if branch in ('', 'None'):
+ branch = None
+ if branch:
+ branch = secure_git_branchname(branch)
+ branch_option = ['-b', branch]
+ else:
+ branch_option = []
+ need_to_restart = False
+ extracted = {}
+ data_files = {'templates': [], 'static': [], 'sources': [], 'interviews': [], 'modules': [], 'questions': []}
+ directory = tempfile.mkdtemp(prefix='SavedFile')
+ output = ''
+ pypi_url = daconfig.get('pypi url', 'https://pypi.org/pypi')
+ expected_name = 'unknown'
+ if github_url:
+ github_url = re.sub(r'[^A-Za-z0-9\-\.\_\~\:\/\#\[\]\@\$\+\,\=]', '', github_url)
+ repo_name = re.sub(r'/*$', '', github_url)
+ repo_name = re.sub(r'^http.*github.com/', '', repo_name)
+ repo_name = re.sub(r'.*@github.com:', '', repo_name)
+ repo_name = re.sub(r'.git$', '', repo_name)
+ if 'x-oauth-basic@github.com' not in github_url and can_publish_to_github and github_email:
+ github_url = f'git@github.com:{repo_name}.git'
+ expected_name = re.sub(r'.*/', '', github_url)
+ expected_name = re.sub(r'\.git', '', expected_name)
+ expected_name = re.sub(r'docassemble-', '', expected_name)
+ (private_key_file, public_key_file) = get_ssh_keys(github_email)
+ os.chmod(private_key_file, stat.S_IRUSR | stat.S_IWUSR)
+ os.chmod(public_key_file, stat.S_IRUSR | stat.S_IWUSR)
+ ssh_script = tempfile.NamedTemporaryFile(mode='w', prefix="datemp", suffix='.sh', delete=False, encoding='utf-8')
+ ssh_script.write('# /bin/bash\n\nssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -i "' + str(private_key_file) + '" $1 $2 $3 $4 $5 $6')
+ ssh_script.close()
+ os.chmod(ssh_script.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
+ # git_prefix = "GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -i \"" + str(private_key_file) + "\"' "
+ git_prefix = "GIT_SSH=" + ssh_script.name + " "
+ git_env = dict(os.environ, GIT_SSH=ssh_script.name)
+ output += "Doing " + git_prefix + "git clone " + " ".join(branch_option) + github_url + "\n"
+ try:
+ output += subprocess.check_output(["git", "clone"] + branch_option + [github_url], cwd=directory, stderr=subprocess.STDOUT, env=git_env).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ return {'action': "error", 'message': "error running git clone. " + output}
+ else:
+ if not github_url.startswith('http'):
+ github_url = f'https://github.com/{repo_name}'
+ expected_name = re.sub(r'.*/', '', github_url)
+ expected_name = re.sub(r'\.git', '', expected_name)
+ expected_name = re.sub(r'docassemble-', '', expected_name)
+ try:
+ if branch is not None:
+ logmessage("Doing git clone -b " + branch + " " + github_url)
+ output += subprocess.check_output(['git', 'clone', '-b', branch, github_url], cwd=directory, stderr=subprocess.STDOUT).decode()
+ else:
+ logmessage("Doing git clone " + github_url)
+ output += subprocess.check_output(['git', 'clone', github_url], cwd=directory, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ return {'action': "error", 'message': "error running git clone. " + output}
+ logmessage(output)
+ dirs_inside = [f for f in os.listdir(directory) if os.path.isdir(os.path.join(directory, f)) and re.search(r'^[A-Za-z0-9]', f)]
+ if len(dirs_inside) == 1:
+ commit_file = os.path.join(directory_for(area['playgroundpackages'], current_project), '.' + dirs_inside[0])
+ packagedir = os.path.join(directory, dirs_inside[0])
+ try:
+ current_commit = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output = err.output.decode()
+ return {'action': "error", 'message': "error running git rev-parse. " + output}
+ with open(commit_file, 'w', encoding='utf-8') as fp:
+ fp.write(current_commit.strip())
+ logmessage("Wrote " + current_commit.strip() + " to " + commit_file)
+ else:
+ logmessage("Did not find a single directory inside repo")
+ if pull_only:
+ return {'action': 'pull_only'}
+ elif pypi_package:
+ pypi_package = re.sub(r'[^A-Za-z0-9\-\.\_\:\/\@\+\=]', '', pypi_package)
+ pypi_package = 'docassemble.' + re.sub(r'^docassemble\.', '', pypi_package)
+ package_file = tempfile.NamedTemporaryFile(suffix='.tar.gz')
+ try:
+ http = httplib2.Http()
+ resp, content = http.request(pypi_url + "/" + str(pypi_package) + "/json", "GET")
+ the_pypi_url = None
+ if int(resp['status']) == 200:
+ pypi_response = json.loads(content.decode())
+ for file_option in pypi_response['releases'][pypi_response['info']['version']]:
+ if file_option['packagetype'] == 'sdist':
+ the_pypi_url = file_option['url']
+ break
+ else:
+ return {'action': 'fail', 'message': word("The package you specified could not be downloaded from PyPI.")}
+ if the_pypi_url is None:
+ return {'action': 'fail', 'message': word("The package you specified could not be downloaded from PyPI as a tar.gz file.")}
+ except BaseException as err:
+ return {'action': 'error', 'message': "error getting information about PyPI package. " + str(err)}
+ try:
+ urlretrieve(the_pypi_url, package_file.name)
+ except BaseException as err:
+ return {'action': 'error', 'message': "error downloading PyPI package. " + str(err)}
+ try:
+ tar = tarfile.open(package_file.name)
+ tar.extractall(path=directory)
+ tar.close()
+ except BaseException as err:
+ return {'action': 'error', 'message': "error unpacking PyPI package. " + str(err)}
+ package_file.close()
+ initial_directories = len(splitall(directory)) + 1
+ for root, dirs, files in os.walk(directory):
+ at_top_level = bool(('setup.py' in files or 'pyproject.toml' in files or 'setup.cfg' in files) and 'docassemble' in dirs)
+ for a_file in files:
+ orig_file = os.path.join(root, a_file)
+ # output += "Original file is " + orig_file + "\n"
+ thefilename = os.path.join(*splitall(orig_file)[initial_directories:]) # pylint: disable=no-value-for-parameter
+ (the_directory, filename) = os.path.split(thefilename)
+ if filename.startswith('#') or filename.endswith('~'):
+ continue
+ dirparts = splitall(the_directory)
+ if '.git' in dirparts:
+ continue
+ levels = re.findall(r'/', the_directory)
+ for sec in ('templates', 'static', 'sources', 'questions'):
+ if the_directory.endswith('data/' + sec) and filename != 'README.md':
+ data_files[sec].append(filename)
+ target_filename = os.path.join(directory_for(area[area_sec[sec]], current_project), filename)
+ # output += "Copying " + orig_file + "\n"
+ copy_if_different(orig_file, target_filename)
+ if filename == 'README.md' and at_top_level:
+ with open(orig_file, 'r', encoding='utf-8') as fp:
+ readme_text = fp.read()
+ if filename == '.gitignore' and at_top_level:
+ with open(orig_file, 'r', encoding='utf-8') as fp:
+ gitignore_text = fp.read()
+ if filename == 'setup.py' and at_top_level:
+ with open(orig_file, 'r', encoding='utf-8') as fp:
+ setup_py = fp.read()
+ if filename == 'pyproject.toml' and at_top_level:
+ with open(orig_file, 'r', encoding='utf-8') as fp:
+ pyproject_toml = fp.read()
+ elif len(levels) >= 1 and not at_top_level and filename.endswith('.py') and filename != '__init__.py' and 'tests' not in dirparts and 'data' not in dirparts:
+ data_files['modules'].append(filename)
+ target_filename = os.path.join(directory_for(area['playgroundmodules'], current_project), filename)
+ # output += "Copying " + orig_file + "\n"
+ if (not os.path.isfile(target_filename)) or filecmp.cmp(orig_file, target_filename) is False:
+ need_to_restart = True
+ copy_if_different(orig_file, target_filename)
+ # output += "setup.py is " + str(len(setup_py)) + " characters long\n"
+ if setup_py:
+ setup_py = re.sub(r'.*setup\(', '', setup_py, flags=re.DOTALL)
+ for line in setup_py.splitlines():
+ m = re.search(r"^ *([a-z_]+) *= *\(?'(.*)'", line)
+ if m:
+ extracted[m.group(1)] = m.group(2)
+ m = re.search(r'^ *([a-z_]+) *= *\(?"(.*)"', line)
+ if m:
+ extracted[m.group(1)] = m.group(2)
+ m = re.search(r'^ *([a-z_]+) *= *\[(.*)\]', line)
+ if m:
+ the_list = []
+ for item in re.split(r', *', m.group(2)):
+ inner_item = re.sub(r"'$", '', item)
+ inner_item = re.sub(r"^'", '', inner_item)
+ inner_item = re.sub(r'"+$', '', inner_item)
+ inner_item = re.sub(r'^"+', '', inner_item)
+ the_list.append(inner_item)
+ extracted[m.group(1)] = the_list
+ if pyproject_toml:
+ data = tomli.loads(pyproject_toml)
+ if 'project' in data and isinstance(data['project'], dict):
+ extracted['description'] = data['project'].get('description', '')
+ extracted['name'] = data['project'].get('name', '')
+ extracted['version'] = data['project'].get('version', '')
+ extracted['license'] = data['project'].get('license', '')
+ if 'authors' in data['project'] and isinstance(data['project']['authors'], list) and len(data['project']['authors']) > 0 and isinstance(data['project']['authors'][0], dict):
+ extracted['author'] = data['project']['authors'][0].get('name', '')
+ extracted['author_email'] = data['project']['authors'][0].get('email', '')
+ if 'dependencies' in data['project'] and isinstance(data['project']['dependencies'], list):
+ extracted['install_requires'] = data['project']['dependencies']
+ if 'urls' in data['project'] and isinstance(data['project']['urls'], dict):
+ extracted['url'] = data['project']['urls'].get('Homepage', '')
+ if not extracted.get('name', None):
+ return {'action': 'error', 'message': "could not find name of PyPI package."}
+ info_dict = {'readme': readme_text, 'gitignore': gitignore_text, 'interview_files': data_files['questions'], 'sources_files': data_files['sources'], 'static_files': data_files['static'], 'module_files': data_files['modules'], 'template_files': data_files['templates'], 'dependencies': extracted.get('install_requires', []), 'description': extracted.get('description', ''), 'author_name': extracted.get('author', ''), 'author_email': extracted.get('author_email', ''), 'license': extracted.get('license', ''), 'url': extracted.get('url', ''), 'version': extracted.get('version', ''), 'github_url': github_url, 'github_branch': branch, 'pypi_package_name': pypi_package}
+ info_dict['dependencies'] = [x.strip() for x in map(lambda y: re.sub(r'[\>\<\=@].*', '', y), info_dict['dependencies']) if x not in ('docassemble', 'docassemble.base', 'docassemble.webapp')]
+ # output += "info_dict is set\n"
+ package_name = re.sub(r'^docassemble\.', '', extracted.get('name', expected_name))
+ # if not user_can_edit_package(pkgname='docassemble.' + package_name):
+ # index = 1
+ # orig_package_name = package_name
+ # while index < 100 and not user_can_edit_package(pkgname='docassemble.' + package_name):
+ # index += 1
+ # package_name = orig_package_name + str(index)
+ with open(os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + package_name), 'w', encoding='utf-8') as fp:
+ the_yaml = standardyaml.safe_dump(info_dict, default_flow_style=False, default_style='|')
+ fp.write(str(the_yaml))
+ for sec in area:
+ area[sec].finalize()
+ for key in r.keys('da:interviewsource:docassemble.playground' + str(playground_user.id) + ':*'):
+ r.incr(key.decode())
+ return {'action': 'finished', 'need_to_restart': need_to_restart, 'package_name': package_name}
+
+
+def get_github_username_and_email():
+ storage = RedisCredStorage(oauth_app='github')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ raise DAException('GitHub integration expired.')
+ http = credentials.authorize(httplib2.Http())
+ try:
+ resp, content = http.request("https://api.github.com/user", "GET")
+ except:
+ return None, None, None
+ if int(resp['status']) == 200:
+ info = json.loads(content.decode('utf-8', 'ignore'))
+ github_user_name = info.get('login', None)
+ github_author_name = info.get('name', None)
+ github_email = info.get('email', None)
+ else:
+ raise DAError("playground_packages: could not get information about GitHub User")
+ if github_email is None:
+ resp, content = http.request("https://api.github.com/user/emails", "GET")
+ if int(resp['status']) == 200:
+ info = json.loads(content.decode('utf-8', 'ignore'))
+ for item in info:
+ if item.get('email', None) and item.get('visibility', None) != 'private':
+ github_email = item['email']
+ if github_user_name is None or github_email is None:
+ raise DAError("playground_packages: login not present in user info from GitHub")
+ return github_user_name, github_email, github_author_name
+
+
+@develop_bp.route('/playgroundpackages', methods=['GET', 'POST'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_packages():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ setup_translation()
+ fix_package_folder()
+ playground_user = get_playground_user()
+ current_project = get_current_project()
+ form = PlaygroundPackagesForm(request.form)
+ fileform = PlaygroundUploadForm(request.form)
+ the_file = secure_filename_spaces_ok(request.args.get('file', ''))
+ # no_file_specified = bool(the_file == '')
+ scroll = False
+ allow_pypi = daconfig.get('pypi', False)
+ pypi_username = current_user.pypi_username
+ pypi_password = current_user.pypi_password
+ pypi_url = daconfig.get('pypi url', 'https://pypi.org/pypi')
+ can_publish_to_pypi = bool(allow_pypi is True and pypi_username is not None and pypi_password is not None and pypi_username != '' and pypi_password != '')
+ github_auth_info = {}
+ if current_app.config['USE_GITHUB']:
+ github_auth = r.get('da:using_github:userid:' + str(current_user.id))
+ if github_auth is not None:
+ github_auth = github_auth.decode()
+ if github_auth == '1':
+ github_auth_info = {'shared': True, 'orgs': True}
+ else:
+ github_auth_info = json.loads(github_auth)
+ can_publish_to_github = True
+ else:
+ can_publish_to_github = False
+ else:
+ can_publish_to_github = None
+ if can_publish_to_github and request.method == 'GET':
+ storage = RedisCredStorage(oauth_app='github')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ state_string = random_string(16)
+ session['github_next'] = json.dumps({'state': state_string, 'path': 'playground_packages', 'arguments': request.args})
+ flow = get_github_flow()
+ uri = flow.step1_get_authorize_url(state=state_string)
+ return redirect(uri)
+ show_message = true_or_false(request.args.get('show_message', True))
+ github_message = None
+ pypi_message = None
+ pypi_version = None
+ package_list, package_auth = get_package_info() # pylint: disable=unused-variable
+ package_names = sorted([package.package.name for package in package_list])
+ for default_package in ('docassemble', 'docassemble.base', 'docassemble.webapp'):
+ if default_package in package_names:
+ package_names.remove(default_package)
+ # if the_file:
+ # scroll = True
+ if request.method == 'GET':
+ is_new = true_or_false(request.args.get('new', False))
+ else:
+ is_new = False
+ if is_new:
+ # scroll = True
+ the_file = ''
+ area = {}
+ file_list = {}
+ section_name = {'playground': 'Interview files', 'playgroundpackages': 'Packages', 'playgroundtemplate': 'Template files', 'playgroundstatic': 'Static files', 'playgroundsources': 'Source files', 'playgroundmodules': 'Modules'}
+ section_sec = {'playgroundtemplate': 'template', 'playgroundstatic': 'static', 'playgroundsources': 'sources', 'playgroundmodules': 'modules'}
+ section_field = {'playground': form.interview_files, 'playgroundtemplate': form.template_files, 'playgroundstatic': form.static_files, 'playgroundsources': form.sources_files, 'playgroundmodules': form.module_files}
+ for sec in ('playground', 'playgroundpackages', 'playgroundtemplate', 'playgroundstatic', 'playgroundsources', 'playgroundmodules'):
+ area[sec] = SavedFile(playground_user.id, fix=True, section=sec)
+ the_directory = directory_for(area[sec], current_project)
+ if sec == 'playground' and current_project != 'default' and not os.path.isdir(the_directory):
+ current_project = set_current_project('default')
+ the_directory = directory_for(area[sec], current_project)
+ if os.path.isdir(the_directory):
+ file_list[sec] = sorted([f for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9]', f)])
+ else:
+ file_list[sec] = []
+ for sec, field in section_field.items():
+ the_list = []
+ for item in file_list[sec]:
+ the_list.append((item, item))
+ field.choices = the_list
+ the_list = []
+ for item in package_names:
+ the_list.append((item, item))
+ form.dependencies.choices = the_list
+ validated = False
+ form.github_branch.choices = []
+ if form.github_branch.data:
+ form.github_branch.choices.append((form.github_branch.data, form.github_branch.data))
+ else:
+ form.github_branch.choices.append(('', ''))
+ if request.method == 'POST' and not (current_app.config['DEVELOPER_CAN_INSTALL'] or current_user.has_role('admin')):
+ form.install_also.data = 'n'
+ form.install.data = ''
+ if request.method == 'POST' and 'uploadfile' not in request.files:
+ the_file = form.file_name.data
+ the_file = re.sub(r'[^A-Za-z0-9\-\_\.]+', '-', the_file)
+ the_file = re.sub(r'^docassemble-', r'', the_file)
+ form.files_to_add.choices = [('.gitignore', '.gitignore'), ('LICENSE', 'LICENSE'), ('MANIFEST.in', 'MANIFEST.in'), ('README.md', 'README.md'), ('pyproject.toml', 'pyproject.toml'), ('setup.cfg', 'setup.cfg'), ('setup.py', 'setup.py'), ('docassemble/' + the_file + '/__init__.py', 'docassemble/' + the_file + '/__init__.py')]
+ for sec, prefix in (('playground', 'data/questions/'), ('playgroundtemplate', 'data/templates/'), ('playgroundstatic', 'data/static/'), ('playgroundsources', 'data/sources/'), ('playgroundmodules', '')):
+ if sec not in ('playground', 'playgroundmodules'):
+ form.files_to_add.choices.append(('docassemble/' + the_file + '/' + prefix + 'README.md', 'docassemble/' + the_file + '/' + prefix + 'README.md'))
+ for item in file_list[sec]:
+ path = 'docassemble/' + the_file + '/' + prefix + item
+ form.files_to_add.choices.append((path, path))
+ if request.method == 'POST' and 'uploadfile' not in request.files and form.validate():
+ validated = True
+ the_directory = directory_for(area['playgroundpackages'], current_project)
+ files = sorted([f for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9]', f)])
+ editable_files = []
+ for a_file in files:
+ editable_files.append({'name': re.sub(r'^docassemble\.', r'', a_file), 'modtime': os.path.getmtime(os.path.join(the_directory, a_file))})
+ assign_opacity(editable_files)
+ editable_file_listing = [x['name'] for x in editable_files]
+ if request.method == 'GET' and not the_file and not is_new:
+ current_file = get_current_file(current_project, 'packages')
+ if not current_file.startswith('docassemble.'):
+ current_file = 'docassemble.' + current_file
+ set_current_file(current_project, 'packages', current_file)
+ if re.sub(r'^docassemble\.', r'', current_file) in editable_file_listing:
+ the_file = re.sub(r'^docassemble\.', r'', current_file)
+ else:
+ delete_current_file(current_project, 'packages')
+ if len(editable_files) > 0:
+ the_file = sorted(editable_files, key=lambda x: x['modtime'])[-1]['name']
+ else:
+ the_file = ''
+ # if the_file != '' and not user_can_edit_package(pkgname='docassemble.' + the_file):
+ # flash(word('Sorry, that package name,') + ' ' + the_file + word(', is already in use by someone else'), 'error')
+ # validated = False
+ if request.method == 'GET' and the_file in editable_file_listing:
+ set_current_file(current_project, 'packages', 'docassemble.' + the_file)
+ if the_file == '' and len(file_list['playgroundpackages']) and not is_new:
+ the_file = file_list['playgroundpackages'][0]
+ the_file = re.sub(r'^docassemble\.', r'', the_file)
+ old_info = {}
+ branch_info = []
+ github_http = None
+ github_ssh = None
+ github_use_ssh = False
+ github_user_name = None
+ github_email = None
+ github_author_name = None
+ github_url_from_file = None
+ pypi_package_from_file = None
+ expected_name = 'unknown'
+ if request.method == 'GET' and the_file != '':
+ if the_file != '' and os.path.isfile(os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + the_file)):
+ filename = os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + the_file)
+ with open(filename, 'r', encoding='utf-8') as fp:
+ content = fp.read()
+ old_info = standardyaml.load(content, Loader=standardyaml.FullLoader)
+ if isinstance(old_info, dict):
+ if 'license' in old_info and isinstance(old_info['license'], str) and 'MIT License' in old_info['license']:
+ old_info['license'] = 'MIT'
+ github_url_from_file = old_info.get('github_url', None)
+ pypi_package_from_file = old_info.get('pypi_package_name', None)
+ for field in ('license', 'description', 'author_name', 'author_email', 'version', 'url', 'readme'):
+ if field in old_info:
+ form[field].data = old_info[field]
+ else:
+ form[field].data = ''
+ if 'dependencies' in old_info and isinstance(old_info['dependencies'], list) and len(old_info['dependencies']):
+ old_info['dependencies'] = list(map(lambda y: re.sub(r'[\>\<\=].*', '', y), old_info['dependencies']))
+ for item in ('docassemble', 'docassemble.base', 'docassemble.webapp'):
+ if item in old_info['dependencies']:
+ del old_info['dependencies'][item]
+ for field in ('dependencies', 'interview_files', 'template_files', 'module_files', 'static_files', 'sources_files'):
+ if field in old_info and isinstance(old_info[field], list) and len(old_info[field]):
+ form[field].data = old_info[field]
+ else:
+ raise DAException("YAML yielded " + repr(old_info) + " from " + repr(content))
+ else:
+ filename = None
+ if the_file != '' and can_publish_to_github and not is_new:
+ github_package_name = 'docassemble-' + the_file
+ try:
+ storage = RedisCredStorage(oauth_app='github')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ if form.github.data:
+ state_string = random_string(16)
+ session['github_next'] = json.dumps({'state': state_string, 'path': 'playground_packages', 'arguments': request.args})
+ flow = get_github_flow()
+ uri = flow.step1_get_authorize_url(state=state_string)
+ return redirect(uri)
+ raise DAException('GitHub integration expired.')
+ http = credentials.authorize(httplib2.Http())
+ resp, content = http.request("https://api.github.com/user", "GET")
+ if int(resp['status']) == 200:
+ info = json.loads(content.decode('utf-8', 'ignore'))
+ github_user_name = info.get('login', None)
+ github_author_name = info.get('name', None)
+ github_email = info.get('email', None)
+ else:
+ raise DAError("playground_packages: could not get information about GitHub User")
+ if github_email is None:
+ resp, content = http.request("https://api.github.com/user/emails", "GET")
+ if int(resp['status']) == 200:
+ info = json.loads(content.decode('utf-8', 'ignore'))
+ for item in info:
+ if item.get('email', None) and item.get('visibility', None) != 'private':
+ github_email = item['email']
+ if github_user_name is None or github_email is None:
+ raise DAError("playground_packages: login not present in user info from GitHub")
+ found = False
+ found_strong = False
+ resp, content = http.request("https://api.github.com/repos/" + str(github_user_name) + "/" + github_package_name, "GET")
+ if int(resp['status']) == 200:
+ repo_info = json.loads(content.decode('utf-8', 'ignore'))
+ github_http = repo_info['html_url']
+ github_ssh = repo_info['ssh_url']
+ if repo_info['private']:
+ github_use_ssh = True
+ github_message = word('This package is') + ' ' + word("published on GitHub") + '.'
+ if github_author_name:
+ github_message += " " + word("The author is") + " " + github_author_name + "."
+ branch_info = get_branch_info(http, repo_info['full_name'])
+ found = True
+ if github_url_from_file is None or github_url_from_file in [github_ssh, github_http]:
+ found_strong = True
+ if found_strong is False and github_auth_info.get('shared'):
+ repositories = get_user_repositories(http)
+ for repo_info in repositories:
+ if repo_info['name'] != github_package_name or (github_http is not None and github_http == repo_info['html_url']) or (github_ssh is not None and github_ssh == repo_info['ssh_url']):
+ continue
+ if found and github_url_from_file is not None and github_url_from_file not in [repo_info['html_url'], repo_info['ssh_url']]:
+ break
+ github_http = repo_info['html_url']
+ github_ssh = repo_info['ssh_url']
+ if repo_info['private']:
+ github_use_ssh = True
+ github_message = word('This package is') + ' ' + word("published on GitHub") + '.'
+ branch_info = get_branch_info(http, repo_info['full_name'])
+ found = True
+ if github_url_from_file is None or github_url_from_file in [github_ssh, github_http]:
+ found_strong = True
+ break
+ if found_strong is False and github_auth_info['orgs']:
+ orgs_info = get_orgs_info(http)
+ for org_info in orgs_info:
+ resp, content = http.request("https://api.github.com/repos/" + str(org_info['login']) + "/" + github_package_name, "GET")
+ if int(resp['status']) == 200:
+ repo_info = json.loads(content.decode('utf-8', 'ignore'))
+ if found and github_url_from_file is not None and github_url_from_file not in [repo_info['html_url'], repo_info['ssh_url']]:
+ break
+ github_http = repo_info['html_url']
+ github_ssh = repo_info['ssh_url']
+ if repo_info['private']:
+ github_use_ssh = True
+ github_message = word('This package is') + ' ' + word("published on GitHub") + '.'
+ branch_info = get_branch_info(http, repo_info['full_name'])
+ found = True
+ if github_url_from_file is None or github_url_from_file in [github_ssh, github_http]:
+ found_strong = True
+ break
+ if found is False:
+ github_message = word('This package is not yet published on your GitHub account.')
+ except BaseException as e:
+ logmessage('playground_packages: GitHub error. ' + str(e))
+ github_message = word('Unable to determine if the package is published on your GitHub account.')
+ if request.method == 'POST' and 'uploadfile' in request.files:
+ the_files = request.files.getlist('uploadfile')
+ need_to_restart = False
+ if current_user.timezone:
+ the_timezone = zoneinfo.ZoneInfo(current_user.timezone)
+ else:
+ the_timezone = zoneinfo.ZoneInfo(get_default_timezone())
+ if the_files:
+ for up_file in the_files:
+ # zip_filename = werkzeug.utils.secure_filename(up_file.filename)
+ zippath = tempfile.NamedTemporaryFile(mode="wb", suffix=".zip", prefix="datemp", delete=False)
+ up_file.save(zippath.name)
+ area_sec = {'templates': 'playgroundtemplate', 'static': 'playgroundstatic', 'sources': 'playgroundsources', 'questions': 'playground'}
+ zippath.close()
+ with zipfile.ZipFile(zippath.name, mode='r') as zf:
+ readme_text = ''
+ gitignore_text = ''
+ setup_py = ''
+ pyproject_toml = ''
+ extracted = {}
+ data_files = {'templates': [], 'static': [], 'sources': [], 'interviews': [], 'modules': [], 'questions': []}
+ has_docassemble_dir = set()
+ has_setup_file = set()
+ for zinfo in zf.infolist():
+ if zinfo.is_dir():
+ if zinfo.filename.endswith('/docassemble/'):
+ has_docassemble_dir.add(re.sub(r'/docassemble/$', '', zinfo.filename))
+ if zinfo.filename == 'docassemble/':
+ has_docassemble_dir.add('')
+ elif zinfo.filename.endswith('/setup.py') or zinfo.filename.endswith('/pyproject.toml') or zinfo.filename.endswith('/setup.cfg'):
+ (directory, filename) = os.path.split(zinfo.filename)
+ has_setup_file.add(directory)
+ elif zinfo.filename in ('setup.py', 'pyproject.toml', 'setup.cfg'):
+ has_setup_file.add('')
+ root_dir = None
+ for directory in has_docassemble_dir.union(has_setup_file):
+ if root_dir is None or len(directory) < len(root_dir):
+ root_dir = directory
+ if root_dir is None:
+ flash(word("The zip file did not contain a docassemble add-on package."), 'error')
+ return redirect(url_for('develop.playground_packages', project=current_project, file=the_file))
+ for zinfo in zf.infolist():
+ # logmessage("Found a " + zinfo.filename)
+ if zinfo.filename.endswith('/'):
+ continue
+ (directory, filename) = os.path.split(zinfo.filename)
+ if filename.startswith('#') or filename.endswith('~'):
+ continue
+ dirparts = splitall(directory)
+ if '.git' in dirparts:
+ continue
+ levels = re.findall(r'/', directory)
+ time_tuple = zinfo.date_time
+ the_time = time.mktime(datetime.datetime(*time_tuple).timetuple())
+ for sec in ('templates', 'static', 'sources', 'questions'):
+ if directory.endswith('data/' + sec) and filename != 'README.md':
+ data_files[sec].append(filename)
+ target_filename = os.path.join(directory_for(area[area_sec[sec]], current_project), filename)
+ with zf.open(zinfo) as source_fp, open(target_filename, 'wb') as target_fp:
+ shutil.copyfileobj(source_fp, target_fp)
+ os.utime(target_filename, (the_time, the_time))
+ if filename == 'README.md' and directory == root_dir:
+ with zf.open(zinfo) as f:
+ the_file_obj = TextIOWrapper(f, encoding='utf8')
+ readme_text = the_file_obj.read()
+ if filename == '.gitignore' and directory == root_dir:
+ with zf.open(zinfo) as f:
+ the_file_obj = TextIOWrapper(f, encoding='utf8')
+ gitignore_text = the_file_obj.read()
+ if filename == 'setup.py' and directory == root_dir:
+ with zf.open(zinfo) as f:
+ the_file_obj = TextIOWrapper(f, encoding='utf8')
+ setup_py = the_file_obj.read()
+ if filename == 'pyproject.toml' and directory == root_dir:
+ with zf.open(zinfo) as f:
+ the_file_obj = TextIOWrapper(f, encoding='utf8')
+ pyproject_toml = the_file_obj.read()
+ elif len(levels) >= 1 and directory != root_dir and filename.endswith('.py') and filename != '__init__.py' and 'tests' not in dirparts and 'data' not in dirparts:
+ need_to_restart = True
+ data_files['modules'].append(filename)
+ target_filename = os.path.join(directory_for(area['playgroundmodules'], current_project), filename)
+ with zf.open(zinfo) as source_fp, open(target_filename, 'wb') as target_fp:
+ shutil.copyfileobj(source_fp, target_fp)
+ os.utime(target_filename, (the_time, the_time))
+ if setup_py:
+ setup_py = re.sub(r'.*setup\(', '', setup_py, flags=re.DOTALL)
+ for line in setup_py.splitlines():
+ m = re.search(r"^ *([a-z_]+) *= *\(?'(.*)'", line)
+ if m:
+ extracted[m.group(1)] = m.group(2)
+ m = re.search(r'^ *([a-z_]+) *= *\(?"(.*)"', line)
+ if m:
+ extracted[m.group(1)] = m.group(2)
+ m = re.search(r'^ *([a-z_]+) *= *\[(.*)\]', line)
+ if m:
+ the_list = []
+ for item in re.split(r', *', m.group(2)):
+ inner_item = re.sub(r"'$", '', item)
+ inner_item = re.sub(r"^'", '', inner_item)
+ inner_item = re.sub(r'"+$', '', inner_item)
+ inner_item = re.sub(r'^"+', '', inner_item)
+ the_list.append(inner_item)
+ extracted[m.group(1)] = the_list
+ if pyproject_toml:
+ data = tomli.loads(pyproject_toml)
+ if 'project' in data and isinstance(data['project'], dict):
+ extracted['description'] = data['project'].get('description', '')
+ extracted['name'] = data['project'].get('name', '')
+ extracted['version'] = data['project'].get('version', '')
+ extracted['license'] = data['project'].get('license', '')
+ if 'authors' in data['project'] and isinstance(data['project']['authors'], list) and len(data['project']['authors']) > 0 and isinstance(data['project']['authors'][0], dict):
+ extracted['author'] = data['project']['authors'][0].get('name', '')
+ extracted['author_email'] = data['project']['authors'][0].get('email', '')
+ if 'dependencies' in data['project'] and isinstance(data['project']['dependencies'], list):
+ extracted['install_requires'] = data['project']['dependencies']
+ if 'urls' in data['project'] and isinstance(data['project']['urls'], dict):
+ extracted['url'] = data['project']['urls'].get('Homepage', '')
+ info_dict = {'readme': readme_text, 'gitignore': gitignore_text, 'interview_files': data_files['questions'], 'sources_files': data_files['sources'], 'static_files': data_files['static'], 'module_files': data_files['modules'], 'template_files': data_files['templates'], 'dependencies': list(map(lambda y: re.sub(r'[\>\<\=].*', '', y), extracted.get('install_requires', []))), 'description': extracted.get('description', ''), 'author_name': extracted.get('author', ''), 'author_email': extracted.get('author_email', ''), 'license': extracted.get('license', ''), 'url': extracted.get('url', ''), 'version': extracted.get('version', '')}
+
+ info_dict['dependencies'] = [x.strip() for x in map(lambda y: re.sub(r'[\>\<\=@].*', '', y), info_dict['dependencies']) if x not in ('docassemble', 'docassemble.base', 'docassemble.webapp')]
+ package_name = re.sub(r'^docassemble\.', '', extracted.get('name', expected_name))
+ with open(os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + package_name), 'w', encoding='utf-8') as fp:
+ the_yaml = standardyaml.safe_dump(info_dict, default_flow_style=False, default_style='|')
+ fp.write(str(the_yaml))
+ for key in r.keys('da:interviewsource:docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':*'):
+ r.incr(key.decode())
+ for the_area in area.values():
+ the_area.finalize()
+ the_file = package_name
+ zippath.close()
+ if show_message:
+ flash(word("The package was unpacked into the Playground."), 'success')
+ if need_to_restart:
+ return redirect(url_for('main.restart_page', next=url_for('develop.playground_packages', project=current_project, file=the_file)))
+ return redirect(url_for('develop.playground_packages', project=current_project, file=the_file))
+ if request.method == 'GET' and 'pull' in request.args and int(request.args['pull']) == 1 and ('github_url' in request.args or 'pypi' in request.args):
+ if can_publish_to_github and (github_user_name is None or github_email is None):
+ (github_user_name, github_email, github_author_name) = get_github_username_and_email()
+ github_url = request.args.get('github_url', None)
+ pypi_package = request.args.get('pypi', None)
+ branch = request.args.get('branch', None)
+ do_pypi_also = true_or_false(request.args.get('pypi_also', False))
+ if current_app.config['DEVELOPER_CAN_INSTALL'] or current_user.has_role('admin'):
+ do_install_also = true_or_false(request.args.get('install_also', False))
+ else:
+ do_install_also = False
+ result = do_playground_pull(area, current_project, github_url=github_url, branch=branch, pypi_package=pypi_package, can_publish_to_github=can_publish_to_github, github_email=github_email, pull_only=(do_pypi_also or do_install_also))
+ if result['action'] == 'error':
+ raise DAError("playground_packages: " + result['message'])
+ if result['action'] == 'fail':
+ flash(result['message'], 'error')
+ return redirect(url_for('develop.playground_packages', project=current_project))
+ if result['action'] == 'pull_only':
+ the_args = {'package': the_file, 'project': current_project}
+ if do_pypi_also:
+ the_args['pypi'] = '1'
+ if do_install_also:
+ the_args['install'] = '1'
+ area['playgroundpackages'].finalize()
+ return redirect(url_for('develop.create_playground_package', **the_args))
+ if result['action'] == 'finished':
+ the_file = result['package_name']
+ if show_message:
+ flash(word("The package was unpacked into the Playground."), 'success')
+ # shutil.rmtree(directory)
+ if result['need_to_restart']:
+ return redirect(url_for('main.restart_page', next=url_for('develop.playground_packages', file=the_file, project=current_project)))
+ return redirect(url_for('develop.playground_packages', project=current_project, file=the_file))
+ if request.method == 'POST' and validated and form.delete.data and the_file != '' and the_file == form.file_name.data and os.path.isfile(os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + the_file)):
+ os.remove(os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + the_file))
+ dotfile = os.path.join(directory_for(area['playgroundpackages'], current_project), '.docassemble-' + the_file)
+ if os.path.exists(dotfile):
+ os.remove(dotfile)
+ area['playgroundpackages'].finalize()
+ flash(word("Deleted package"), "success")
+ return redirect(url_for('develop.playground_packages', project=current_project))
+ if not is_new:
+ pkgname = 'docassemble.' + the_file
+ if can_publish_to_pypi:
+ pypi_info = pypi_status(pkgname)
+ if pypi_info['error']:
+ pypi_message = word("Unable to determine if the package is published on PyPI.")
+ else:
+ if pypi_info['exists'] and 'info' in pypi_info['info']:
+ pypi_version = pypi_info['info']['info'].get('version', None)
+ pypi_message = word('This package is') + ' ' + word("published on PyPI") + '.'
+ pypi_author = pypi_info['info']['info'].get('author', None)
+ if pypi_author:
+ pypi_message += " " + word("The author is") + " " + pypi_author + "."
+ if pypi_version != form['version'].data:
+ pypi_message += " " + word("The version on PyPI is") + " " + str(pypi_version) + ". " + word("Your version is") + " " + str(form['version'].data) + "."
+ else:
+ pypi_message = word('This package is not yet published on PyPI.')
+ if request.method == 'POST' and validated:
+ new_info = {}
+ for field in ('license', 'description', 'author_name', 'author_email', 'version', 'url', 'readme', 'dependencies', 'interview_files', 'template_files', 'module_files', 'static_files', 'sources_files'):
+ new_info[field] = form[field].data
+ # logmessage("found " + str(new_info))
+ if form.submit.data or form.download.data or form.install.data or form.pypi.data or form.github.data:
+ if the_file != '':
+ area['playgroundpackages'].finalize()
+ if form.original_file_name.data and form.original_file_name.data != the_file:
+ old_filename = os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + form.original_file_name.data)
+ if os.path.isfile(old_filename):
+ os.remove(old_filename)
+ if can_publish_to_pypi and form.pypi.data and pypi_version is not None:
+ if not new_info['version']:
+ new_info['version'] = pypi_version
+ while 'releases' in pypi_info['info'] and new_info['version'] in pypi_info['info']['releases'].keys():
+ versions = new_info['version'].split(".")
+ versions[-1] = str(int(versions[-1]) + 1)
+ new_info['version'] = ".".join(versions)
+ filename = os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + the_file)
+ if os.path.isfile(filename):
+ with open(filename, 'r', encoding='utf-8') as fp:
+ content = fp.read()
+ old_info = standardyaml.load(content, Loader=standardyaml.FullLoader)
+ for name in ('github_url', 'github_branch', 'pypi_package_name', 'gitignore'):
+ if old_info.get(name, None):
+ new_info[name] = old_info[name]
+ with open(filename, 'w', encoding='utf-8') as fp:
+ the_yaml = standardyaml.safe_dump(new_info, default_flow_style=False, default_style='|')
+ fp.write(str(the_yaml))
+ area['playgroundpackages'].finalize()
+ if form.download.data:
+ return redirect(url_for('develop.create_playground_package', package=the_file, project=current_project))
+ if form.install.data:
+ return redirect(url_for('develop.create_playground_package', package=the_file, project=current_project, install='1'))
+ if form.pypi.data:
+ if form.install_also.data:
+ return redirect(url_for('develop.create_playground_package', package=the_file, project=current_project, pypi='1', install='1'))
+ return redirect(url_for('develop.create_playground_package', package=the_file, project=current_project, pypi='1'))
+ if form.github.data:
+ session['github_to_add'] = form.files_to_add.data
+ the_branch = form.github_branch.data
+ if the_branch == "":
+ the_branch = re.sub(r'[^A-Za-z0-9\_\-]', r'', str(form.github_branch_new.data))
+ return redirect(url_for('develop.create_playground_package', project=current_project, package=the_file, github='1', commit_message=form.commit_message.data, new_branch=str(the_branch), pypi_also=('1' if form.pypi_also.data else '0'), install_also=('1' if form.install_also.data else '0')))
+ return redirect(url_for('develop.create_playground_package', project=current_project, package=the_file, github='1', commit_message=form.commit_message.data, branch=str(the_branch), pypi_also=('1' if form.pypi_also.data else '0'), install_also=('1' if form.install_also.data else '0')))
+ the_time = formatted_current_time()
+ if show_message:
+ flash(word('The package information was saved.'), 'success')
+ form.original_file_name.data = the_file
+ form.file_name.data = the_file
+ if the_file != '' and os.path.isfile(os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + the_file)):
+ filename = os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + the_file)
+ else:
+ filename = None
+ header = word("Packages")
+ upload_header = None
+ edit_header = None
+ description = Markup("""Describe your package and choose the files from your Playground that will go into it.""")
+ after_text = None
+ initial_values = playground_values(current_project, the_file)
+ initial_values.update({
+ "daPage": 'package',
+ "daScroll": bool(scroll),
+ "isNew": is_new,
+ "existingFiles": files,
+ "existingPypiVersion": pypi_version,
+ "currentFile": the_file,
+ "daContent": form.readme.data,
+ })
+ initial_values['daTranslations'].update({
+ "needToIncrement": word("You need to increment the version before publishing to PyPI."),
+ "commit": word("Commit"),
+ "publish": word("Publish"),
+ "github": word("GitHub"),
+ "pypi": word("PyPI"),
+ "unsavedChangesWarning": word("There are unsaved changes. Are you sure you wish to leave this page?"),
+ "sureDeletePackage": word("Are you sure that you want to delete this package?"),
+ "packageExistsWarning": word("Warning: a package definition by that name already exists. If you save, you will overwrite it."),
+ })
+ any_files = len(editable_files) > 0
+ back_button = Markup('')
+ if can_publish_to_pypi:
+ if pypi_message is not None:
+ pypi_message = Markup(pypi_message)
+ else:
+ pypi_message = None
+ extra_js = f"""
+
+ {redis_script(initial_values)}
+"""
+ if github_use_ssh:
+ the_github_url = github_ssh
+ else:
+ the_github_url = github_http
+ if the_github_url is None and github_url_from_file is not None:
+ the_github_url = github_url_from_file
+ if the_github_url is None:
+ the_pypi_package_name = pypi_package_from_file
+ else:
+ the_pypi_package_name = None
+ if github_message is not None and github_url_from_file is not None and github_url_from_file != github_http and github_url_from_file != github_ssh:
+ github_message += ' ' + word("This package was originally pulled from") + ' ' + word('a GitHub repository') + '.'
+ if github_message is not None and old_info.get('github_branch', None) and (github_http or github_url_from_file):
+ html_url = github_http or github_url_from_file
+ commit_code = None
+ current_commit_file = os.path.join(directory_for(area['playgroundpackages'], current_project), '.' + github_package_name)
+ if os.path.isfile(current_commit_file):
+ with open(current_commit_file, 'r', encoding='utf-8') as fp:
+ commit_code = fp.read().strip()
+ if current_user.timezone:
+ the_timezone = zoneinfo.ZoneInfo(current_user.timezone)
+ else:
+ the_timezone = zoneinfo.ZoneInfo(get_default_timezone())
+ commit_code_date = datetime.datetime.fromtimestamp(os.path.getmtime(current_commit_file), datetime.timezone.utc).astimezone(the_timezone).strftime("%Y-%m-%d %H:%M:%S %Z")
+ else:
+ commit_code_date = ''
+ if commit_code:
+ github_message += ' ' + word('The current branch is %s and the current commit is %s.') % ('' + old_info['github_branch'] + '', '' + commit_code[0:7] + '') + ' ' + word('The commit was saved locally at %s.') % commit_code_date
+ else:
+ github_message += ' ' + word('The current branch is %s.') % ('' + old_info['github_branch'] + '',)
+ if github_message is not None:
+ github_message = Markup(github_message)
+ branch = old_info.get('github_branch', None)
+ if branch is not None:
+ branch = branch.strip()
+ branch_choices = []
+ if len(branch_info) > 0:
+ branch_choices.append(("", word("(New branch)")))
+ branch_names = set()
+ for br in branch_info:
+ branch_names.add(br['name'])
+ branch_choices.append((br['name'], br['name']))
+ if branch and branch in branch_names:
+ form.github_branch.data = branch
+ default_branch = branch
+ elif 'master' in branch_names:
+ form.github_branch.data = 'master'
+ default_branch = 'master'
+ elif 'main' in branch_names:
+ form.github_branch.data = 'main'
+ default_branch = 'main'
+ else:
+ default_branch = GITHUB_BRANCH
+ form.github_branch.choices = branch_choices
+ if form.author_name.data in ('', None) and current_user.first_name and current_user.last_name:
+ form.author_name.data = current_user.first_name + " " + current_user.last_name
+ if form.author_email.data in ('', None) and current_user.email:
+ form.author_email.data = current_user.email
+ if current_user.id != playground_user.id:
+ header += " / " + playground_user.email
+ if current_project != 'default':
+ header += " / " + current_project
+ response = make_response(render_template('develop/playgroundpackages.html', current_project=current_project, branch=default_branch, version_warning=None, bodyclass='daadminbody', can_publish_to_pypi=can_publish_to_pypi, pypi_message=pypi_message, can_publish_to_github=can_publish_to_github, github_message=github_message, github_url=the_github_url, pypi_package_name=the_pypi_package_name, back_button=back_button, tab_title=header, page_title=header, extra_css=Markup('\n '), extra_js=Markup(extra_js), header=header, upload_header=upload_header, edit_header=edit_header, description=description, form=form, fileform=fileform, files=files, file_list=file_list, userid=playground_user.id, editable_files=sorted(editable_files, key=lambda y: y['name'].lower()), current_file=the_file, after_text=after_text, section_name=section_name, section_sec=section_sec, section_field=section_field, package_names=sorted(package_names, key=lambda y: y.lower()), any_files=any_files), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+def github_as_http(url):
+ if url.startswith('http'):
+ return url
+ return re.sub(r'^[^@]+@([^:]+):(.*)\.git$', r'https://\1/\2', url)
+
+
+def copy_if_different(source, destination):
+ if (not os.path.isfile(destination)) or filecmp.cmp(source, destination) is False:
+ shutil.copyfile(source, destination)
+
+
+@develop_bp.route('/playground_redirect_poll', methods=['GET'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_redirect_poll():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ playground_user = get_playground_user()
+ key = 'da:runplayground:' + str(playground_user.id)
+ the_url = r.get(key)
+ # logmessage("playground_redirect: key " + str(key) + " is " + str(the_url))
+ if the_url is not None:
+ the_url = the_url.decode()
+ r.delete(key)
+ return jsonify({'success': True, 'url': the_url})
+ return jsonify({'success': False, 'url': the_url})
+
+
+@develop_bp.route('/playground_redirect', methods=['GET', 'POST'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_redirect():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ playground_user = get_playground_user()
+ key = 'da:runplayground:' + str(playground_user.id)
+ counter = 0
+ while counter < 15:
+ the_url = r.get(key)
+ # logmessage("playground_redirect: key " + str(key) + " is " + str(the_url))
+ if the_url is not None:
+ the_url = the_url.decode()
+ r.delete(key)
+ return redirect(the_url)
+ time.sleep(1)
+ counter += 1
+ return ('File not found', 404)
+
+
+def upload_js():
+ return """
+ $("#uploadlink").on('click', function(event){
+ $("#uploadlabel").click();
+ event.preventDefault();
+ return false;
+ });
+ $("#uploadlabel").on('click', function(event){
+ event.stopPropagation();
+ event.preventDefault();
+ $("#uploadfile").click();
+ return false;
+ });
+ $("#uploadfile").on('click', function(event){
+ event.stopPropagation();
+ });
+ $("#uploadfile").on('change', function(event){
+ $("#fileform").submit();
+ });"""
+
+
+def variables_js(form=None, office_mode=False, current_project=None):
+ if current_project is None:
+ current_project = 'default'
+ playground_user = get_playground_user()
+ output = """
+function activatePopovers(){
+ var daPopoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
+ var daPopoverList = daPopoverTriggerList.map(function (daPopoverTriggerEl) {
+ return new bootstrap.Popover(daPopoverTriggerEl, {trigger: "click", html: true});
+ });
+}
+
+function activateVariables(){
+ $(".daparenthetical").on("click", function(event){
+ var reference = $(this).data("ref");
+ //console.log("reference is " + reference);
+ var target = $('[data-name="' + reference + '"]').first();
+ if (target.length > 0){
+ //console.log("target is " + target);
+ //console.log("scrolltop is now " + $('#daplaygroundcard').scrollTop());
+ //console.log("Scrolling to " + target.parent().parent().position().top);
+ $('#daplaygroundcard').animate({
+ scrollTop: target.parent().parent().position().top
+ }, 1000);
+ }
+ event.preventDefault();
+ });
+
+ $(".dashowmethods").on("click", function(event){
+ var target_id = $(this).data("showhide");
+ $("#" + target_id).slideToggle();
+ });
+
+ $(".dashowattributes").each(function(){
+ var basename = $(this).data('name');
+ if (attrs_showing.hasOwnProperty(basename)){
+ if (attrs_showing[basename]){
+ $('tr[data-parent="' + basename + '"]').show();
+ }
+ }
+ else{
+ attrs_showing[basename] = false;
+ }
+ });
+
+ $(".dashowattributes").on("click", function(event){
+ var basename = $(this).data('name');
+ attrs_showing[basename] = !attrs_showing[basename];
+ $('tr[data-parent="' + basename + '"]').each(function(){
+ $(this).toggle();
+ });
+ });"""
+ if office_mode:
+ return output + "\n}"
+ if form is None:
+ form = 'form'
+ output += """
+ $(".playground-variable").on("click", function(event){
+ daCm.ev.dispatch(daCm.ev.state.replaceSelection($(this).data("insert"), "around"));
+ daCm.ev.focus();
+ });
+
+ $(".dasearchicon").on("click", function(event){
+ var query = $(this).data('name');
+ if (query == null || query.length == 0){
+ daCm.ev.dispatch({selection: {anchor: daCm.ev.state.selection.main.head}})
+ return;
+ }
+ daStartNewSearch(daCm.ev, query);
+ event.preventDefault();
+ return false;
+ });
+}
+
+var interviewBaseUrl = '""" + url_for('interview.index', reset='1', cache='0', i='docassemble.playground' + str(playground_user.id) + ':.yml') + """';
+var shareBaseUrl = '""" + url_for('interview.index', i='docassemble.playground' + str(playground_user.id) + ':.yml', _external=True) + """';
+
+function updateRunLink(){
+ if (currentProject == 'default'){
+ $("#daRunButton").attr("href", interviewBaseUrl.replace(':.yml', ':' + $("#daVariables").val()));
+ $("a.da-example-share").attr("href", shareBaseUrl.replace(':.yml', ':' + $("#daVariables").val()));
+ }
+ else{
+ $("#daRunButton").attr("href", interviewBaseUrl.replace(':.yml', currentProject + ':' + $("#daVariables").val()));
+ $("a.da-example-share").attr("href", shareBaseUrl.replace(':.yml', currentProject + ':' + $("#daVariables").val()));
+ }
+}
+
+function fetchVars(changed){
+ $("#playground_content").val(daCm.ev.state.doc.toString());
+ updateRunLink();
+ $.ajax({
+ type: "POST",
+ url: """ + '"' + url_for('develop.playground_variables') + '"' + """ + '?project=' + currentProject,
+ data: 'csrf_token=' + $("#""" + form + """ input[name='csrf_token']").val() + '&variablefile=' + $("#daVariables").val() + '&ajax=1&changed=' + (changed ? 1 : 0),
+ success: function(data){
+ if (data.action && data.action == 'reload'){
+ location.reload(true);
+ }
+ if (data.vocab_list != null){
+ vocab = data.vocab_list;
+ }
+ if (data.current_project != null){
+ currentProject = data.current_project;
+ }
+ if (data.ac_list != null){
+ daAutoComp.length = 0;
+ let n = data.ac_list.length;
+ for(let i = 0; i < n; i++){
+ daAutoComp.push(data.ac_list[i]);
+ }
+ }
+ if (data.variables_html != null){
+ $("#daplaygroundtable").html(data.variables_html);
+ var daPopoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
+ var daPopoverList = daPopoverTriggerList.map(function (daPopoverTriggerEl) {
+ return new bootstrap.Popover(daPopoverTriggerEl, {trigger: "focus", html: true});
+ });
+ activateVariables();
+ }
+ },
+ dataType: 'json'
+ });
+ $("#daVariables").blur();
+}
+
+function variablesReady(){
+ $("#daVariables").change(function(event){
+ fetchVars(true);
+ });
+}
+
+function daFetchVariableReportCallback(data){
+ var translations = """ + json.dumps({'in mako': word("in mako"), 'mentioned in': word("mentioned in"), 'defined by': word("defined by")}) + """;
+ var modal = $("#daVariablesReport .modal-body");
+ if (modal.length == 0){
+ console.log("No modal body on page");
+ return;
+ }
+ if (!data.success){
+ $(modal).html('""" + word("Failed to load report") + """
');
+ return;
+ }
+ var yaml_file = data.yaml_file;
+ modal.empty();
+ var accordion = $('');
+ accordion.addClass("accordion");
+ accordion.attr("id", "varsreport");
+ var n = data.items.length;
+ for (var i = 0; i < n; ++i){
+ var item = data.items[i];
+ if (item.questions.length){
+ var accordionItem = $('');
+ accordionItem.addClass("accordion-item");
+ var accordionItemHeader = $('');
+ accordionItemHeader.addClass("accordion-header");
+ accordionItemHeader.attr("id", "accordionItemheader" + i);
+ accordionItemHeader.html('');
+ accordionItem.append(accordionItemHeader);
+ var collapse = $("");
+ collapse.attr("id", "collapse" + i);
+ collapse.attr("aria-labelledby", "accordionItemheader" + i);
+ collapse.data("bs-parent", "#varsreport");
+ collapse.addClass("accordion-collapse");
+ collapse.addClass("collapse");
+ var accordionItemBody = $("");
+ accordionItemBody.addClass("accordion-body");
+ var m = item.questions.length;
+ for (var j = 0; j < m; j++){
+ var h5 = $("");
+ h5.html(item.questions[j].usage.map(x => translations[x]).join(','));
+ var pre = $("");
+ pre.html(item.questions[j].source_code);
+ accordionItemBody.append(h5);
+ accordionItemBody.append(pre);
+ if (item.questions[j].yaml_file != yaml_file){
+ var p = $("");
+ p.html(""" + json.dumps(word("from")) + """ + ' ' + item.questions[j].yaml_file);
+ accordionItemBody.append(p);
+ }
+ }
+ collapse.append(accordionItemBody);
+ accordionItem.append(collapse);
+ accordion.append(accordionItem);
+ }
+ }
+ modal.append(accordion);
+}
+
+function daFetchVariableReport(theFile=currentFile){
+ url = """ + json.dumps(url_for('develop.variables_report', project=current_project)) + """ + "&file=" + theFile;
+ $("#daVariablesReport .modal-body").html('
""" + word("Loading . . .") + """
');
+ $.ajax({
+ type: "GET",
+ url: url,
+ success: daFetchVariableReportCallback,
+ xhrFields: {
+ withCredentials: true
+ },
+ error: function(xhr, status, error){
+ $("#daVariablesReport .modal-body").html('""" + word("Failed to load report") + """
');
+ }
+ });
+}
+
+$( document ).ready(function() {
+ $(document).on('keydown', function(e){
+ if (e.which == 13){
+ var tag = $( document.activeElement ).prop("tagName");
+ if (tag == "INPUT"){
+ e.preventDefault();
+ e.stopPropagation();
+ daCm.ev.focus();
+ return false;
+ }
+ }
+ });
+});
+"""
+ return output
+
+
+@develop_bp.route("/varsreport", methods=['GET'])
+@login_required
+@roles_required(['admin', 'developer'])
+def variables_report():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ setup_translation()
+ playground_user = get_playground_user()
+ playground = SavedFile(playground_user.id, fix=True, section='playground')
+ the_file = request.args.get('file', None)
+ if the_file is not None:
+ the_file = secure_filename_spaces_ok(the_file)
+ current_project = werkzeug.utils.secure_filename(request.args.get('project', 'default'))
+ the_directory = directory_for(playground, current_project)
+ files = sorted([f for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9]', f)])
+ if len(files) == 0:
+ return jsonify(success=False, reason=1)
+ if the_file is None or the_file not in files:
+ return jsonify(success=False, reason=2)
+ interview_source = interview_source_from_string('docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + the_file, raise_jinja_errors=False)
+ interview_source.set_testing(True)
+ interview = Interview(source=interview_source)
+ ensure_ml_file_exists(interview, the_file, current_project)
+ yaml_file = 'docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + the_file
+ the_current_info = current_info(yaml=yaml_file, req=request, action=None, device_id=request.cookies.get('ds', None))
+ this_thread.current_info = the_current_info
+ interview_status = InterviewStatus(current_info=the_current_info)
+ variables_html, vocab_list, vocab_dict, ac_list = get_vars_in_use(interview, interview_status, debug_mode=False, current_project=current_project) # pylint: disable=unused-variable
+ results = []
+ result_dict = {}
+ for name in vocab_list:
+ if name in ('x', 'row_item', 'i', 'j', 'k', 'l', 'm', 'n') or name.startswith('x.') or name.startswith('x[') or name.startswith('row_item.'):
+ continue
+ result = {'name': name, 'questions': []}
+ results.append(result)
+ result_dict[name] = result
+ for question in interview.questions_list:
+ names_seen = {}
+ for the_type, the_set in (('in mako', question.mako_names), ('mentioned in', question.names_used), ('defined by', question.fields_used)):
+ for name in the_set:
+ the_name = name
+ subnames = [the_name]
+ while True:
+ if re.search(r'\[[^\]]\]$', the_name):
+ the_name = re.sub(r'\[[^\]]\]$', '', the_name)
+ elif '.' in the_name:
+ the_name = re.sub(r'\.[^\.]*$', '', the_name)
+ else:
+ break
+ subnames.append(the_name)
+ on_first = True
+ for subname in subnames:
+ if the_type == 'defined by' and not on_first:
+ the_type = 'mentioned in'
+ on_first = False
+ if subname not in result_dict:
+ continue
+ if subname not in names_seen:
+ names_seen[subname] = {'yaml_file': question.from_source.path, 'source_code': question.source_code.strip(), 'usage': []}
+ result_dict[subname]['questions'].append(names_seen[subname])
+ if the_type not in names_seen[subname]['usage']:
+ names_seen[subname]['usage'].append(the_type)
+ return jsonify(success=True, yaml_file=yaml_file, items=results)
+
+
+@develop_bp.route('/playgroundvariables', methods=['POST'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_variables():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ playground_user = get_playground_user()
+ current_project = get_current_project()
+ playground = SavedFile(playground_user.id, fix=True, section='playground')
+ the_directory = directory_for(playground, current_project)
+ files = sorted([f for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9]', f)])
+ if len(files) == 0:
+ return jsonify(success=False, reason=1)
+ post_data = request.form.copy()
+ if request.method == 'POST' and 'variablefile' in post_data:
+ active_file = post_data['variablefile']
+ if post_data['variablefile'] in files:
+ if 'changed' in post_data and int(post_data['changed']):
+ set_variable_file(current_project, active_file)
+ interview_source = interview_source_from_string('docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + active_file, raise_jinja_errors=False)
+ interview_source.set_testing(True)
+ else:
+ if active_file == '' and current_project == 'default':
+ active_file = 'test.yml'
+ content = ''
+ if 'playground_content' in post_data:
+ content = re.sub(r'\r\n', r'\n', post_data['playground_content'])
+ interview_source = InterviewSourceString(content=content, directory=the_directory, package="docassemble.playground" + str(playground_user.id) + project_name(current_project), path="docassemble.playground" + str(playground_user.id) + project_name(current_project) + ":" + active_file, testing=True)
+ interview = Interview(source=interview_source)
+ ensure_ml_file_exists(interview, active_file, current_project)
+ the_current_info = current_info(yaml='docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + active_file, req=request, action=None, device_id=request.cookies.get('ds', None))
+ this_thread.current_info = the_current_info
+ interview_status = InterviewStatus(current_info=the_current_info)
+ variables_html, vocab_list, vocab_dict, ac_list = get_vars_in_use(interview, interview_status, debug_mode=False, current_project=current_project) # pylint: disable=unused-variable
+ return jsonify(success=True, variables_html=variables_html, vocab_list=vocab_list, current_project=current_project, ac_list=ac_list)
+ return jsonify(success=False, reason=2)
+
+
+def ensure_ml_file_exists(interview, yaml_file, current_project):
+ playground_user = get_playground_user()
+ if len(interview.mlfields) > 0:
+ if hasattr(interview, 'ml_store'):
+ parts = interview.ml_store.split(':')
+ if parts[0] != 'docassemble.playground' + str(playground_user.id) + current_project:
+ return
+ source_filename = re.sub(r'.*/', '', parts[1])
+ else:
+ source_filename = 'ml-' + re.sub(r'\.ya?ml$', '', yaml_file) + '.json'
+ # logmessage("Source filename is " + source_filename)
+ source_dir = SavedFile(playground_user.id, fix=False, section='playgroundsources')
+ source_directory = directory_for(source_dir, current_project)
+ if current_project != 'default':
+ source_filename = os.path.join(current_project, source_filename)
+ if source_filename not in source_dir.list_of_files():
+ # logmessage("Source filename does not exist yet")
+ source_dir.fix()
+ source_path = os.path.join(source_directory, source_filename)
+ with open(source_path, 'a', encoding='utf-8'):
+ os.utime(source_path, None)
+ source_dir.finalize()
+
+
+def assign_opacity(files):
+ if len(files) == 1:
+ files[0]['opacity'] = 1.0
+ else:
+ indexno = 0.0
+ max_indexno = float(len(files) - 1)
+ for file_dict in sorted(files, key=lambda x: x['modtime']):
+ file_dict['opacity'] = round(0.2 + 0.8*(indexno/max_indexno), 2)
+ indexno += 1.0
+
+
+@develop_bp.route('/playground_run', methods=['GET', 'POST'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_page_run():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ playground_user = get_playground_user()
+ current_project = get_current_project()
+ the_file = secure_filename_spaces_ok(request.args.get('file'))
+ if the_file:
+ active_interview_string = 'docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + the_file
+ the_url = url_for('interview.index', reset=1, i=active_interview_string)
+ key = 'da:runplayground:' + str(playground_user.id)
+ # logmessage("Setting key " + str(key) + " to " + str(the_url))
+ pipe = r.pipeline()
+ pipe.set(key, the_url)
+ pipe.expire(key, 25)
+ pipe.execute()
+ return redirect(url_for('develop.playground_page', file=the_file, project=current_project))
+ return redirect(url_for('develop.playground_page', project=current_project))
+
+
+def get_list_of_projects(user_id):
+ playground = SavedFile(user_id, fix=False, section='playground')
+ return playground.list_of_dirs()
+
+
+def rename_project(user_id, old_name, new_name):
+ fix_package_folder()
+ for sec in ('', 'sources', 'static', 'template', 'modules', 'packages'):
+ area = SavedFile(user_id, fix=True, section='playground' + sec)
+ if os.path.isdir(os.path.join(area.directory, old_name)):
+ os.rename(os.path.join(area.directory, old_name), os.path.join(area.directory, new_name))
+ area.finalize()
+
+
+def create_project(user_id, new_name):
+ fix_package_folder()
+ for sec in ('', 'sources', 'static', 'template', 'modules', 'packages'):
+ area = SavedFile(user_id, fix=True, section='playground' + sec)
+ new_dir = os.path.join(area.directory, new_name)
+ if not os.path.isdir(new_dir):
+ os.makedirs(new_dir, exist_ok=True)
+ path = os.path.join(new_dir, '.placeholder')
+ with open(path, 'a', encoding='utf-8'):
+ os.utime(path, None)
+ area.finalize()
+
+
+def delete_project(user_id, the_project_name):
+ fix_package_folder()
+ for sec in ('', 'sources', 'static', 'template', 'modules', 'packages'):
+ area = SavedFile(user_id, fix=True, section='playground' + sec)
+ area.delete_directory(the_project_name)
+ area.finalize()
+
+
+@develop_bp.route('/playgroundproject', methods=['GET', 'POST'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_project():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ use_gd = bool(current_app.config['USE_GOOGLE_DRIVE'] is True and get_gd_folder() is not None)
+ use_od = bool(use_gd is False and current_app.config['USE_ONEDRIVE'] is True and get_od_folder() is not None)
+ playground_user = get_playground_user()
+ current_project = get_current_project()
+ if request.args.get('rename'):
+ form = RenameProject(request.form)
+ mode = 'rename'
+ description = word("You are renaming the project called %s.") % (current_project, )
+ page_title = word("Rename project")
+ if request.method == 'POST' and form.validate():
+ if current_project == 'default':
+ flash(word("You cannot rename the default Playground project"), 'error')
+ else:
+ rename_project(playground_user.id, current_project, form.name.data)
+ if use_gd:
+ try:
+ rename_gd_project(current_project, form.name.data)
+ except BaseException as the_err:
+ logmessage("playground_project: unable to rename project on Google Drive. " + str(the_err))
+ elif use_od:
+ try:
+ rename_od_project(current_project, form.name.data)
+ except BaseException as the_err:
+ try:
+ logmessage("playground_project: unable to rename project on OneDrive. " + str(the_err))
+ except:
+ logmessage("playground_project: unable to rename project on OneDrive.")
+ current_project = set_current_project(form.name.data)
+ flash(word('Since you renamed a project, the server needs to restart in order to reload any modules.'), 'info')
+ return redirect(url_for('main.restart_page', next=url_for('develop.playground_project', project=current_project)))
+ elif request.args.get('new'):
+ form = NewProject(request.form)
+ mode = 'new'
+ description = word("Enter the name of the new project you want to create.")
+ page_title = word("New project")
+ if request.method == 'POST' and form.validate():
+ if form.name.data == 'default' or form.name.data in get_list_of_projects(playground_user.id):
+ flash(word("The project name %s is not available.") % (form.name.data, ), "error")
+ else:
+ create_project(playground_user.id, form.name.data)
+ current_project = set_current_project(form.name.data)
+ mode = 'standard'
+ return redirect(url_for('develop.playground_page', project=current_project))
+ elif request.args.get('delete'):
+ form = DeleteProject(request.form)
+ mode = 'delete'
+ description = word("WARNING! If you press Delete, the contents of the %s project will be permanently deleted.") % (current_project, )
+ page_title = word("Delete project")
+ if request.method == 'POST' and form.validate():
+ if current_project == 'default':
+ flash(word("The default project cannot be deleted."), "error")
+ else:
+ if use_gd:
+ try:
+ trash_gd_project(current_project)
+ except BaseException as the_err:
+ logmessage("playground_project: unable to delete project on Google Drive. " + str(the_err))
+ elif use_od:
+ try:
+ trash_od_project(current_project)
+ except BaseException as the_err:
+ try:
+ logmessage("playground_project: unable to delete project on OneDrive. " + str(the_err))
+ except:
+ logmessage("playground_project: unable to delete project on OneDrive.")
+ delete_project(playground_user.id, current_project)
+ flash(word("The project %s was deleted.") % (current_project,), "success")
+ current_project = set_current_project('default')
+ return redirect(url_for('develop.playground_project', project=current_project))
+ else:
+ form = None
+ mode = 'standard'
+ page_title = word("Projects")
+ description = word("You can divide up your Playground into multiple separate areas, apart from your default Playground area. Each Project has its own question files and Folders.")
+ back_button = Markup('')
+ response = make_response(render_template('develop/manage_projects.html', version_warning=None, bodyclass='daadminbody', back_button=back_button, tab_title=word("Projects"), description=description, page_title=page_title, projects=get_list_of_projects(playground_user.id), current_project=current_project, mode=mode, form=form), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+def set_current_project(new_name):
+ key = 'da:playground:project:' + str(current_user.id)
+ pipe = r.pipeline()
+ pipe.set(key, new_name)
+ pipe.expire(key, 2592000)
+ pipe.execute()
+ return new_name
+
+
+def get_current_project():
+ current_project = request.args.get('project', None)
+ if current_project is not None:
+ current_project = werkzeug.utils.secure_filename(current_project)
+ key = 'da:playground:project:' + str(current_user.id)
+ if current_project is None:
+ current_project = r.get(key)
+ if current_project is not None:
+ current_project = current_project.decode()
+ else:
+ pipe = r.pipeline()
+ pipe.set(key, current_project)
+ pipe.expire(key, 2592000)
+ pipe.execute()
+ if current_project is None:
+ return 'default'
+ return current_project
+
+
+def set_current_file(current_project, section, new_name):
+ key = 'da:playground:project:' + str(current_user.id) + ':playground' + section + ':' + current_project
+ pipe = r.pipeline()
+ pipe.set(key, new_name)
+ pipe.expire(key, 2592000)
+ pipe.execute()
+ return new_name
+
+
+def get_current_file(current_project, section):
+ key = 'da:playground:project:' + str(current_user.id) + ':playground' + section + ':' + current_project
+ current_file = r.get(key)
+ if current_file is None:
+ return ''
+ return current_file.decode()
+
+
+def delete_current_file(current_project, section):
+ key = 'da:playground:project:' + str(current_user.id) + ':playground' + section + ':' + current_project
+ r.delete(key)
+
+
+def clear_current_playground_info():
+ r.delete('da:playground:project:' + str(current_user.id))
+ to_delete = []
+ for key in r.keys('da:playground:project:' + str(current_user.id) + ':playground*'):
+ to_delete.append(key)
+ for key in to_delete:
+ r.delete(key)
+
+
+def set_variable_file(current_project, variable_file):
+ key = 'da:playground:project:' + str(current_user.id) + ':' + current_project + ':variablefile'
+ pipe = r.pipeline()
+ pipe.set(key, variable_file)
+ pipe.expire(key, 2592000)
+ pipe.execute()
+ return variable_file
+
+
+def get_variable_file(current_project):
+ key = 'da:playground:project:' + str(current_user.id) + ':' + current_project + ':variablefile'
+ variable_file = r.get(key)
+ if variable_file is not None:
+ variable_file = variable_file.decode()
+ return variable_file
+
+
+def delete_variable_file(current_project):
+ key = 'da:playground:project:' + str(current_user.id) + ':' + current_project + ':variablefile'
+ r.delete(key)
+
+
+def get_list_of_playgrounds():
+ user_list = []
+ for user in db.session.execute(select(UserModel.id, UserModel.social_id, UserModel.email, UserModel.first_name, UserModel.last_name).join(UserRoles, UserModel.id == UserRoles.user_id).join(Role, UserRoles.role_id == Role.id).where(and_(UserModel.active == True, or_(Role.name == 'admin', Role.name == 'developer'))).distinct().order_by(UserModel.id)): # noqa: E712 # pylint: disable=singleton-comparison
+ if user.social_id.startswith('disabled$'):
+ continue
+ user_info = {}
+ for attrib in ('id', 'email'):
+ user_info[attrib] = getattr(user, attrib)
+ name_string = ''
+ if user.first_name:
+ name_string += str(user.first_name) + " "
+ if user.last_name:
+ name_string += str(user.last_name)
+ user_info['name'] = name_string
+ user_list.append(user_info)
+ return user_list
+
+
+@develop_bp.route('/playgroundselect', methods=['GET', 'POST'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_select():
+ setup_translation()
+ if not (current_app.config['ENABLE_PLAYGROUND'] and current_app.config['ENABLE_SHARING_PLAYGROUNDS']):
+ return ('File not found', 404)
+ current_project = get_current_project()
+ if request.args.get('select'):
+ clear_current_playground_info()
+ set_playground_user(int(request.args['select']))
+ return redirect(url_for('develop.playground_page', project='default'))
+ form = None
+ mode = 'standard'
+ page_title = word("All Playgrounds")
+ description = word("You can use the Playground of another user who has admin or developer privileges.")
+ back_button = Markup('')
+ response = make_response(render_template('develop/manage_playgrounds.html', version_warning=None, bodyclass='daadminbody', back_button=back_button, tab_title=word("All Playgrounds"), description=description, page_title=page_title, playgrounds=get_list_of_playgrounds(), current_project=current_project, mode=mode, form=form), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route("/pgcodecache", methods=['GET'])
+@login_required
+@roles_required(['developer', 'admin'])
+def get_pg_var_cache():
+ response = make_response(bytesyaml.dump_to_bytes(pg_code_cache), 200)
+ response.headers['Content-Disposition'] = 'attachment; filename=pgcodecache.yml'
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ response.headers['Content-Type'] = 'text/plain; charset=utf-8'
+ return response
+
+
+def playground_values(current_project, the_file, playground_user=None):
+ values = {
+ "currentProject": current_project,
+ "currentFile": the_file,
+ "daNotificationContainer": NOTIFICATION_CONTAINER,
+ "daNotificationMessage": NOTIFICATION_MESSAGE,
+ "daSessionLifetimeSeconds": 999 * int(daconfig.get('session lifetime seconds', 43200)),
+ "daUrlPlaygroundPage": url_for('develop.playground_page'),
+ "daUrlPlaygroundPageWithProject": url_for('develop.playground_page', project=current_project),
+ "daGoogleDriveSyncUrl": url_for('develop.sync_with_google_drive', project=current_project, auto_next=url_for('develop.playground_page_run', file=the_file, project=current_project)),
+ "daOneDriveSyncUrl": url_for('develop.sync_with_onedrive', project=current_project, auto_next=url_for('develop.playground_page_run', file=the_file, project=current_project)),
+ "daWrapLines": bool(daconfig.get('wrap lines in playground', True)),
+ "daKeymap": keymap,
+ "daTranslations": {"in mako": word("in mako"),
+ "mentioned in": word("mentioned in"),
+ "defined by": word("defined by"),
+ "from": word("from"),
+ "loading": word("Loading . . ."),
+ "failedToLoad": word("Failed to load report"),
+ "sessionHasExpired": word("Your browser session has expired and you have been signed out. You will not be able to save your work. Please log in again."),
+ "fileExistWarning": word("Warning: a file by that name already exists. If you save, you will overwrite it."),
+ "linkCopiedClipboard": word('Link copied to clipboard.'),
+ "unsavedChangesWarning": word("There are unsaved changes. Are you sure you wish to leave this page?"),
+ "sureYouWantToDelete": word("Are you sure that you want to delete this playground file?"),
+ "sureYouWantToDeleteFile": word("Are you sure that you want to delete this file?"),
+ },
+ }
+ if playground_user:
+ values.update({
+ "interviewBaseUrl": url_for('interview.index', reset='1', cache='0', i='docassemble.playground' + str(playground_user.id) + ':.yml'),
+ "shareBaseUrl": url_for('interview.index', i='docassemble.playground' + str(playground_user.id) + ':.yml', _external=True),
+ "daVariablesReportUrl": url_for('develop.variables_report', project=current_project),
+ "daUrlPlaygroundVariables": url_for('develop.playground_variables')
+ })
+ return values
+
+
+@develop_bp.route('/playground', methods=['GET', 'POST'])
+@login_required
+@roles_required(['developer', 'admin'])
+def playground_page():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ playground_user = get_playground_user()
+ current_project = get_current_project()
+ if 'ajax' in request.form and int(request.form['ajax']):
+ is_ajax = True
+ use_gd = False
+ use_od = False
+ else:
+ is_ajax = False
+ use_gd = bool(current_app.config['USE_GOOGLE_DRIVE'] is True and get_gd_folder() is not None)
+ use_od = bool(use_gd is False and current_app.config['USE_ONEDRIVE'] is True and get_od_folder() is not None)
+ if request.method == 'GET' and needs_to_change_password():
+ return redirect(url_for('user.change_password', next=url_for('develop.playground_page', project=current_project)))
+ fileform = PlaygroundUploadForm(request.form)
+ form = PlaygroundForm(request.form)
+ interview = None
+ the_file = secure_filename_spaces_ok(request.args.get('file', get_current_file(current_project, 'questions')))
+ valid_form = None
+ if request.method == 'POST':
+ valid_form = form.validate()
+ if request.method == 'GET':
+ is_new = true_or_false(request.args.get('new', False))
+ debug_mode = true_or_false(request.args.get('debug', False))
+ else:
+ debug_mode = False
+ is_new = bool(not valid_form and form.status.data == 'new')
+ if is_new:
+ the_file = ''
+ playground = SavedFile(playground_user.id, fix=True, section='playground')
+ the_directory = directory_for(playground, current_project)
+ if current_project != 'default' and not os.path.isdir(the_directory):
+ current_project = set_current_project('default')
+ the_directory = directory_for(playground, current_project)
+ if request.method == 'POST' and 'uploadfile' in request.files:
+ the_files = request.files.getlist('uploadfile')
+ if the_files:
+ for up_file in the_files:
+ try:
+ filename = secure_filename(up_file.filename)
+ extension, mimetype = get_ext_and_mimetype(filename) # pylint: disable=unused-variable
+ if extension not in ('yml', 'yaml'):
+ flash(word("Sorry, only YAML files can be uploaded here. To upload other types of files, use the Folders."), 'error')
+ return redirect(url_for('develop.playground_page', project=current_project))
+ filename = re.sub(r'[^A-Za-z0-9\-\_\. ]+', '_', filename)
+ new_file = filename
+ filename = os.path.join(the_directory, filename)
+ up_file.save(filename)
+ try:
+ with open(filename, 'r', encoding='utf-8') as fp:
+ fp.read()
+ except:
+ os.remove(filename)
+ flash(word("There was a problem reading the YAML file you uploaded. Are you sure it is a YAML file? File was not saved."), 'error')
+ return redirect(url_for('develop.playground_page', project=current_project))
+ playground.finalize()
+ r.incr('da:interviewsource:docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + new_file)
+ flash(word("Uploaded %s to the Playground.") % (os.path.basename(filename),), 'success')
+ return redirect(url_for('develop.playground_page', project=current_project, file=os.path.basename(filename)))
+ except BaseException as err_mess:
+ flash("Error of type " + str(type(err_mess)) + " processing upload: " + str(err_mess), "error")
+ return redirect(url_for('develop.playground_page', project=current_project))
+ if request.method == 'POST' and (form.submit.data or form.run.data or form.delete.data):
+ if valid_form and form.playground_name.data:
+ the_file = secure_filename_spaces_ok(form.playground_name.data)
+ # the_file = re.sub(r'[^A-Za-z0-9\_\-\. ]', '', the_file)
+ if the_file != '':
+ if not re.search(r'\.ya?ml$', the_file):
+ the_file = re.sub(r'\..*', '', the_file) + '.yml'
+ filename = os.path.join(the_directory, the_file)
+ if not os.path.isfile(filename):
+ with open(filename, 'a', encoding='utf-8'):
+ os.utime(filename, None)
+ else:
+ # flash(word('You need to type in a name for the interview'), 'error')
+ is_new = True
+ else:
+ # flash(word('You need to type in a name for the interview'), 'error')
+ is_new = True
+ # the_file = re.sub(r'[^A-Za-z0-9\_\-\. ]', '', the_file)
+ files = sorted([{'name': f, 'modtime': os.path.getmtime(os.path.join(the_directory, f))} for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9].*[A-Za-z]$', f)], key=lambda x: x['name'])
+ file_listing = [x['name'] for x in files]
+ assign_opacity(files)
+ if valid_form is False:
+ content = form.playground_content.data
+ else:
+ content = ''
+ if the_file and not is_new and the_file not in file_listing:
+ if request.method == 'GET':
+ delete_current_file(current_project, 'questions')
+ return redirect(url_for('develop.playground_page', project=current_project))
+ the_file = ''
+ is_default = False
+ if request.method == 'GET' and not the_file and not is_new:
+ current_file = get_current_file(current_project, 'questions')
+ if current_file in files:
+ the_file = current_file
+ else:
+ delete_current_file(current_project, 'questions')
+ if len(files) > 0:
+ the_file = sorted(files, key=lambda x: x['modtime'])[-1]['name']
+ elif current_project == 'default':
+ the_file = 'test.yml'
+ is_default = True
+ content = default_playground_yaml
+ else:
+ the_file = ''
+ is_default = False
+ content = ''
+ is_new = True
+ if the_file in file_listing:
+ set_current_file(current_project, 'questions', the_file)
+ active_file = the_file
+ current_variable_file = get_variable_file(current_project)
+ if current_variable_file is not None:
+ if current_variable_file in file_listing:
+ active_file = current_variable_file
+ else:
+ delete_variable_file(current_project)
+ if the_file != '':
+ filename = os.path.join(the_directory, the_file)
+ if (valid_form or is_default) and not os.path.isfile(filename):
+ with open(filename, 'w', encoding='utf-8') as fp:
+ fp.write(content)
+ playground.finalize()
+ files = sorted([{'name': f, 'modtime': os.path.getmtime(os.path.join(the_directory, f))} for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9].*[A-Za-z]$', f)], key=lambda x: x['name'])
+ console_messages = []
+ if request.method == 'POST' and the_file != '' and valid_form:
+ if form.delete.data:
+ filename_to_del = os.path.join(the_directory, form.playground_name.data)
+ if os.path.isfile(filename_to_del):
+ os.remove(filename_to_del)
+ flash(word('File deleted.'), 'info')
+ r.delete('da:interviewsource:docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + the_file)
+ if active_file != the_file:
+ r.incr('da:interviewsource:docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + active_file)
+ cloud_trash(use_gd, use_od, 'questions', form.playground_name.data, current_project)
+ playground.finalize()
+ current_variable_file = get_variable_file(current_project)
+ if current_variable_file in (the_file, form.playground_name.data):
+ delete_variable_file(current_project)
+ delete_current_file(current_project, 'questions')
+ return redirect(url_for('develop.playground_page', project=current_project))
+ flash(word('File not deleted. There was an error.'), 'error')
+ if (form.submit.data or form.run.data):
+ if form.original_playground_name.data and form.original_playground_name.data != the_file:
+ old_filename = os.path.join(the_directory, form.original_playground_name.data)
+ if not is_ajax:
+ flash(word("Changed name of interview"), 'success')
+ cloud_trash(use_gd, use_od, 'questions', form.original_playground_name.data, current_project)
+ if os.path.isfile(old_filename):
+ os.remove(old_filename)
+ files = sorted([{'name': f, 'modtime': os.path.getmtime(os.path.join(the_directory, f))} for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9].*[A-Za-z]$', f)], key=lambda x: x['name'])
+ file_listing = [x['name'] for x in files]
+ assign_opacity(files)
+ if active_file == form.original_playground_name.data:
+ active_file = the_file
+ set_variable_file(current_project, active_file)
+ the_time = formatted_current_time()
+ should_save = True
+ the_content = re.sub(r'\r\n', r'\n', form.playground_content.data)
+ if os.path.isfile(filename):
+ with open(filename, 'r', encoding='utf-8') as fp:
+ orig_content = fp.read()
+ if orig_content == the_content:
+ # logmessage("No need to save")
+ should_save = False
+ if should_save:
+ with open(filename, 'w', encoding='utf-8') as fp:
+ fp.write(the_content)
+ if not form.submit.data and active_file != the_file:
+ active_file = the_file
+ set_variable_file(current_project, active_file)
+ this_interview_string = 'docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + the_file
+ active_interview_string = 'docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + active_file
+ r.incr('da:interviewsource:' + this_interview_string)
+ if the_file != active_file:
+ r.incr('da:interviewsource:' + active_interview_string)
+ playground.finalize()
+ clear_cache(this_interview_string)
+ if active_interview_string != this_interview_string:
+ clear_cache(active_interview_string)
+ if not form.submit.data:
+ the_url = url_for('interview.index', reset=1, i=this_interview_string)
+ key = 'da:runplayground:' + str(playground_user.id)
+ # logmessage("Setting key " + str(key) + " to " + str(the_url))
+ pipe = r.pipeline()
+ pipe.set(key, the_url)
+ pipe.expire(key, 12)
+ pipe.execute()
+ try:
+ interview_source = interview_source_from_string(active_interview_string, raise_jinja_errors=False)
+ interview_source.set_testing(True)
+ interview = Interview(source=interview_source)
+ ensure_ml_file_exists(interview, active_file, current_project)
+ yaml = 'docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + active_file
+ session_id_to_use = uid_or_random(yaml)
+ the_current_info = current_info(yaml=yaml, req=request, action=None, device_id=request.cookies.get('ds', None), session_uid=session_id_to_use)
+ the_current_info['session'] = session_id_to_use
+ this_thread.current_info = the_current_info
+ interview_status = InterviewStatus(current_info=the_current_info)
+ variables_html, vocab_list, vocab_dict, ac_list = get_vars_in_use(interview, interview_status, debug_mode=debug_mode, current_project=current_project) # pylint: disable=unused-variable
+ if form.submit.data:
+ flash_message = flash_as_html(word('Saved at') + ' ' + the_time + '.', 'success', is_ajax=is_ajax)
+ else:
+ flash_message = flash_as_html(word('Saved at') + ' ' + the_time + '. ' + word('Running in other tab.'), message_type='success', is_ajax=is_ajax)
+ if interview.issue.get('mandatory_id', False):
+ console_messages.append(word("Note: it is a best practice to tag every mandatory block with an id."))
+ if interview.issue.get('id_collision', False):
+ console_messages.append(word("Note: more than one block uses id") + " " + interview.issue['id_collision'])
+ except DAError:
+ variables_html = None
+ flash_message = flash_as_html(word('Saved at') + ' ' + the_time + '. ' + word('Problem detected.'), message_type='error', is_ajax=is_ajax)
+ if is_ajax:
+ return jsonify(variables_html=variables_html, vocab_list=vocab_list, ac_list=ac_list, flash_message=flash_message, current_project=current_project, console_messages=console_messages, active_file=active_file, active_interview_url=url_for('interview.index', i=active_interview_string, _external=True))
+ else:
+ flash(word('Playground not saved. There was an error.'), 'error')
+ interview_path = None
+ if valid_form is not False and the_file != '':
+ with open(filename, 'r', encoding='utf-8') as fp:
+ form.original_playground_name.data = the_file
+ form.playground_name.data = the_file
+ content = fp.read()
+ # if not form.playground_content.data:
+ # form.playground_content.data = content
+ if active_file != '':
+ is_fictitious = False
+ interview_path = 'docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + active_file
+ if is_default:
+ interview_source = InterviewSourceString(content=content, directory=the_directory, package="docassemble.playground" + str(playground_user.id) + project_name(current_project), path="docassemble.playground" + str(playground_user.id) + project_name(current_project) + ":" + active_file, testing=True)
+ else:
+ interview_source = interview_source_from_string(interview_path, raise_jinja_errors=False)
+ interview_source.set_testing(True)
+ else:
+ is_fictitious = True
+ if current_project == 'default':
+ active_file = 'test.yml'
+ else:
+ is_new = True
+ if form.playground_content.data:
+ content = re.sub(r'\r', '', form.playground_content.data)
+ interview_source = InterviewSourceString(content=content, directory=the_directory, package="docassemble.playground" + str(playground_user.id) + project_name(current_project), path="docassemble.playground" + str(playground_user.id) + project_name(current_project) + ":" + active_file, testing=True)
+ else:
+ interview_source = InterviewSourceString(content='', directory=the_directory, package="docassemble.playground" + str(playground_user.id) + project_name(current_project), path="docassemble.playground" + str(playground_user.id) + project_name(current_project) + ":" + active_file, testing=True)
+ interview = Interview(source=interview_source)
+ if hasattr(interview, 'mandatory_id_issue') and interview.mandatory_id_issue: # pylint: disable=no-member
+ console_messages.append(word("Note: it is a best practice to tag every mandatory block with an id."))
+ yaml = 'docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + active_file
+ session_id_to_use = uid_or_random(yaml)
+ the_current_info = current_info(yaml='docassemble.playground' + str(playground_user.id) + project_name(current_project) + ':' + active_file, req=request, action=None, device_id=request.cookies.get('ds', None), session_uid=session_id_to_use)
+ the_current_info['session'] = session_id_to_use
+ this_thread.current_info = the_current_info
+ interview_status = InterviewStatus(current_info=the_current_info)
+ variables_html, vocab_list, vocab_dict, ac_list = get_vars_in_use(interview, interview_status, debug_mode=debug_mode, current_project=current_project)
+ dropdown_files = [x['name'] for x in files]
+ define_examples()
+ if is_fictitious or is_new:
+ new_active_file = word('(New file)')
+ if new_active_file not in dropdown_files:
+ dropdown_files.insert(0, new_active_file)
+ if is_fictitious:
+ active_file = new_active_file
+ initial_values = playground_values(current_project, the_file, playground_user)
+ initial_values.update({
+ "daPage": 'questions',
+ "isNew": is_new,
+ "existingFiles": file_listing,
+ "daConsoleMessages": console_messages,
+ "daAutoComp": ac_list,
+ "daContent": content,
+ "validForm": valid_form,
+ "vocab": vocab_list,
+ "originalFileName": the_file,
+ "daEncodedExampleData": [pg_ex['encoded_data_dict'], pg_ex['pg_first_id'][0]] if pg_ex['encoded_data_dict'] is not None else None
+ })
+ any_files = len(files) > 0
+ page_title = word("Playground")
+ if current_user.id != playground_user.id:
+ page_title += " / " + playground_user.email
+ if current_project != 'default':
+ page_title += " / " + current_project
+ extra_js = f"""
+
+ {redis_script(initial_values)}"""
+ response = make_response(render_template('develop/playground.html', projects=get_list_of_projects(playground_user.id), current_project=current_project, version_warning=None, bodyclass='daadminbody', use_gd=use_gd, use_od=use_od, userid=playground_user.id, page_title=Markup(page_title), tab_title=word("Playground"), extra_css=Markup('\n '), extra_js=Markup(extra_js), form=form, fileform=fileform, files=sorted(files, key=lambda y: y['name'].lower()), any_files=any_files, dropdown_files=sorted(dropdown_files, key=lambda y: y.lower()), current_file=the_file, active_file=active_file, content=content, variables_html=Markup(variables_html), example_html=pg_ex['encoded_example_html'], interview_path=interview_path, is_new=str(is_new), valid_form=str(valid_form), own_playground=bool(playground_user.id == current_user.id), action=url_for('develop.playground_page', project=current_project)), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/playgroundbundle.css', methods=['GET'])
+def playground_css_bundle():
+ base_path = Path(importlib.resources.files('docassemble.webapp'), 'static')
+ output = ''
+ for parts in [['app', 'pygments.css'], ['bootstrap', 'css', 'bootstrap-icons.css'], ['bootstrap-fileinput', 'css', 'fileinput.css']]:
+ with open(os.path.join(base_path, *parts), encoding='utf-8') as fp:
+ output += fp.read()
+ output += "\n"
+ return Response(output, mimetype='text/css')
+
+@develop_bp.route('/github_menu', methods=['POST', 'GET'])
+@login_required
+@roles_required(['admin', 'developer'])
+def github_menu():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ if not current_app.config['USE_GITHUB']:
+ return ('File not found', 404)
+ setup_translation()
+ form = GitHubForm(request.form)
+ if request.method == 'POST':
+ if form.configure.data:
+ r.delete('da:github:userid:' + str(current_user.id))
+ return redirect(url_for('develop.github_configure'))
+ if form.unconfigure.data:
+ return redirect(url_for('develop.github_unconfigure'))
+ if form.cancel.data:
+ return redirect(url_for('users.user_profile_page'))
+ if form.save.data:
+ info = {}
+ info['shared'] = bool(form.shared.data)
+ info['orgs'] = bool(form.orgs.data)
+ r.set('da:using_github:userid:' + str(current_user.id), json.dumps(info))
+ flash(word("Your GitHub settings were saved."), 'info')
+ uses_github = r.get('da:using_github:userid:' + str(current_user.id))
+ if uses_github is not None:
+ uses_github = uses_github.decode()
+ if uses_github == '1':
+ form.shared.data = True
+ form.orgs.data = True
+ else:
+ info = json.loads(uses_github)
+ form.shared.data = info['shared']
+ form.orgs.data = info['orgs']
+ description = word("Your GitHub integration is currently turned on. Below, you can change which repositories docassemble can access. You can disable GitHub integration if you no longer wish to use it.")
+ else:
+ description = word("If you have a GitHub account, you can turn on GitHub integration. This will allow you to use GitHub as a version control system for packages from inside the Playground.")
+ return render_template('develop/github.html', form=form, version_warning=None, title=word("GitHub Integration"), tab_title=word("GitHub"), page_title=word("GitHub"), description=description, uses_github=uses_github, bodyclass='daadminbody')
+
+
+@develop_bp.route('/github_configure', methods=['POST', 'GET'])
+@login_required
+@roles_required(['admin', 'developer'])
+def github_configure():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ if not current_app.config['USE_GITHUB']:
+ return ('File not found', 404)
+ setup_translation()
+ storage = RedisCredStorage(oauth_app='github')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ state_string = random_string(16)
+ session['github_next'] = json.dumps({'state': state_string, 'path': 'github_configure', 'arguments': request.args})
+ flow = get_github_flow()
+ uri = flow.step1_get_authorize_url(state=state_string)
+ return redirect(uri)
+ http = credentials.authorize(httplib2.Http())
+ found = False
+ try:
+ resp, content = http.request("https://api.github.com/user/emails", "GET")
+ assert int(resp['status']) == 200
+ except:
+ r.delete('da:github:userid:' + str(current_user.id))
+ r.delete('da:using_github:userid:' + str(current_user.id))
+ flash(word("There was a problem connecting to GitHub. Please check your GitHub configuration and try again."), 'danger')
+ return redirect(url_for('develop.github_menu'))
+ user_info_list = json.loads(content.decode())
+ user_info = None
+ for item in user_info_list:
+ if item.get('email', None) and item.get('visibility', None) != 'private':
+ user_info = item
+ if user_info is None:
+ logmessage("github_configure: could not get information about user")
+ r.delete('da:github:userid:' + str(current_user.id))
+ r.delete('da:using_github:userid:' + str(current_user.id))
+ flash(word("There was a problem connecting to GitHub. Please check your GitHub configuration and try again."), 'danger')
+ return redirect(url_for('develop.github_menu'))
+ try:
+ resp, content = http.request("https://api.github.com/user/keys", "GET")
+ assert int(resp['status']) == 200
+ for key in json.loads(content.decode()):
+ if key['title'] == current_app.config['APP_NAME'] or key['title'] == current_app.config['APP_NAME'] + '_user_' + str(current_user.id):
+ found = True
+ except:
+ logmessage("github_configure: could not get information about ssh keys")
+ r.delete('da:github:userid:' + str(current_user.id))
+ r.delete('da:using_github:userid:' + str(current_user.id))
+ flash(word("There was a problem connecting to GitHub. Please check your GitHub configuration and try again."), 'danger')
+ return redirect(url_for('develop.github_menu'))
+ while found is False:
+ next_link = get_next_link(resp)
+ if next_link:
+ resp, content = http.request(next_link, "GET")
+ if int(resp['status']) == 200:
+ for key in json.loads(content.decode()):
+ if key['title'] == current_app.config['APP_NAME'] or key['title'] == current_app.config['APP_NAME'] + '_user_' + str(current_user.id):
+ found = True
+ else:
+ r.delete('da:github:userid:' + str(current_user.id))
+ r.delete('da:using_github:userid:' + str(current_user.id))
+ flash(word("There was a problem connecting to GitHub. Please check your GitHub configuration and try again."), 'danger')
+ return redirect(url_for('develop.github_menu'))
+ else:
+ break
+ if found:
+ flash(word("An SSH key is already installed on your GitHub account. The existing SSH key will not be replaced. Note that if you are connecting to GitHub from multiple docassemble servers, each server needs to have a different appname in the Configuration. If you have problems using GitHub, disable the integration and configure it again."), 'info')
+ if not found:
+ (private_key_file, public_key_file) = get_ssh_keys(user_info['email']) # pylint: disable=unused-variable
+ with open(public_key_file, 'r', encoding='utf-8') as fp:
+ public_key = fp.read()
+ headers = {'Content-Type': 'application/json'}
+ body = json.dumps({'title': current_app.config['APP_NAME'] + '_user_' + str(current_user.id), 'key': public_key})
+ resp, content = http.request("https://api.github.com/user/keys", "POST", headers=headers, body=body)
+ if int(resp['status']) == 201:
+ flash(word("GitHub integration was successfully configured."), 'info')
+ else:
+ logmessage("github_configure: error setting public key")
+ r.delete('da:github:userid:' + str(current_user.id))
+ r.delete('da:using_github:userid:' + str(current_user.id))
+ flash(word("There was a problem connecting to GitHub. Please check your GitHub configuration and try again."), 'danger')
+ return redirect(url_for('develop.github_menu'))
+ r.set('da:using_github:userid:' + str(current_user.id), json.dumps({'shared': True, 'orgs': True}))
+ return redirect(url_for('develop.github_menu'))
+
+
+@develop_bp.route('/github_unconfigure', methods=['POST', 'GET'])
+@login_required
+@roles_required(['admin', 'developer'])
+def github_unconfigure():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ if not current_app.config['USE_GITHUB']:
+ return ('File not found', 404)
+ setup_translation()
+ storage = RedisCredStorage(oauth_app='github')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ state_string = random_string(16)
+ session['github_next'] = json.dumps({'state': state_string, 'path': 'github_unconfigure', 'arguments': request.args})
+ flow = get_github_flow()
+ uri = flow.step1_get_authorize_url(state=state_string)
+ return redirect(uri)
+ http = credentials.authorize(httplib2.Http())
+ ids_to_remove = []
+ try:
+ resp, content = http.request("https://api.github.com/user/keys", "GET")
+ if int(resp['status']) == 200:
+ for key in json.loads(content.decode()):
+ if key['title'] == current_app.config['APP_NAME'] or key['title'] == current_app.config['APP_NAME'] + '_user_' + str(current_user.id):
+ ids_to_remove.append(key['id'])
+ else:
+ raise DAError("github_configure: could not get information about ssh keys")
+ while True:
+ next_link = get_next_link(resp)
+ if next_link:
+ resp, content = http.request(next_link, "GET")
+ if int(resp['status']) == 200:
+ for key in json.loads(content.decode()):
+ if key['title'] == current_app.config['APP_NAME'] or key['title'] == current_app.config['APP_NAME'] + '_user_' + str(current_user.id):
+ ids_to_remove.append(key['id'])
+ else:
+ raise DAError("github_unconfigure: could not get additional information about ssh keys")
+ else:
+ break
+ for id_to_remove in ids_to_remove:
+ resp, content = http.request("https://api.github.com/user/keys/" + str(id_to_remove), "DELETE")
+ if int(resp['status']) != 204:
+ raise DAError("github_unconfigure: error deleting public key " + str(id_to_remove) + ": " + str(resp['status']) + " content: " + content.decode())
+ except:
+ logmessage("Error deleting SSH keys on GitHub")
+ delete_ssh_keys()
+ r.delete('da:github:userid:' + str(current_user.id))
+ r.delete('da:using_github:userid:' + str(current_user.id))
+ flash(word("GitHub integration was successfully disconnected."), 'info')
+ return redirect(url_for('users.user_profile_page'))
+
+
+@develop_bp.route('/github_oauth_callback', methods=['POST', 'GET'])
+@login_required
+@roles_required(['admin', 'developer'])
+def github_oauth_callback():
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ setup_translation()
+ failed = False
+ do_a_redirect = False
+ if not current_app.config['USE_GITHUB']:
+ logmessage('github_oauth_callback: server does not use github')
+ failed = True
+ elif 'github_next' not in session:
+ logmessage('github_oauth_callback: next not in session')
+ failed = True
+ if failed is False:
+ github_next = json.loads(session['github_next'])
+ del session['github_next']
+ if 'code' not in request.args or 'state' not in request.args:
+ logmessage('github_oauth_callback: code and state not in args')
+ failed = True
+ do_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_a_redirect:
+ flash(word("There was a problem connecting to GitHub. Please check your GitHub configuration and try again."), 'danger')
+ return redirect(url_for('develop.github_menu'))
+ return ('File not found', 404)
+ flow = get_github_flow()
+ credentials = flow.step2_exchange(request.args['code'])
+ storage = RedisCredStorage(oauth_app='github')
+ storage.put(credentials)
+ return redirect(github_next['path'], **github_next['arguments'])
+
+
+@develop_bp.route("/vars", methods=['POST', 'GET'])
+def get_variables():
+ yaml_filename = request.args.get('i', None)
+ if yaml_filename is None:
+ return ("Invalid request", 400)
+ session_info = get_session(yaml_filename)
+ if session_info is None:
+ return ("Invalid request", 400)
+ session_id = session_info['uid']
+ if 'visitor_secret' in request.cookies:
+ secret = request.cookies['visitor_secret']
+ else:
+ secret = request.cookies.get('secret', None)
+ if secret is not None:
+ secret = str(secret)
+ # session_cookie_id = request.cookies.get('session', None)
+ if session_id is None or yaml_filename is None:
+ return jsonify(success=False)
+ # logmessage("get_variables: fetch_user_dict")
+ this_thread.current_info = current_info(yaml=yaml_filename, req=request, interface='vars', device_id=request.cookies.get('ds', None))
+ try:
+ steps, user_dict, is_encrypted = fetch_user_dict(session_id, yaml_filename, secret=secret)
+ assert user_dict is not None
+ except:
+ return jsonify(success=False)
+ if (not DEBUG) and '_internal' in user_dict and 'misc' in user_dict['_internal'] and 'variable_access' in user_dict['_internal']['misc'] and user_dict['_internal']['misc']['variable_access'] is False:
+ return jsonify(success=False)
+ variables = serializable_dict(user_dict, include_internal=True)
+ # variables['_internal'] = serializable_dict(user_dict['_internal'])
+ return jsonify(success=True, variables=variables, steps=steps, encrypted=is_encrypted, uid=session_id, i=yaml_filename)
+
+
+@develop_bp.route("/test_embed", methods=['GET'])
+@login_required
+@roles_required(['admin', 'developer'])
+def test_embed():
+ setup_translation()
+ yaml_filename = request.args.get('i', final_default_yaml_filename)
+ user_dict = fresh_dictionary()
+ interview = get_interview(yaml_filename)
+ the_current_info = current_info(yaml=yaml_filename, req=request, action=None, location=None, interface='web', device_id=request.cookies.get('ds', None))
+ this_thread.current_info = the_current_info
+ interview_status = InterviewStatus(current_info=the_current_info)
+ try:
+ interview.assemble(user_dict, interview_status)
+ except:
+ pass
+ current_language = get_language()
+ page_title = word("Embed test")
+ if interview.options.get('analytics on', True):
+ if ga_configured:
+ ga_ids = google_config.get('analytics id')
+ else:
+ ga_ids = None
+ else:
+ ga_ids = None
+ start_part = standard_html_start(interview_language=current_language, debug=False, bootstrap_theme=interview_status.question.interview.get_bootstrap_theme(), external=True, page_title=page_title, social=daconfig['social'], yaml_filename=yaml_filename) + current_app.config['GLOBAL_CSS'] + additional_css(interview_status)
+ scripts = standard_scripts(interview_language=current_language, external=True) + additional_scripts(ga_ids) + current_app.config['GLOBAL_JS']
+ response = make_response(render_template('develop/test_embed.html', scripts=scripts, start_part=start_part, interview_url=url_for('interview.index', i=yaml_filename, js_target='dablock', _external=True), page_title=page_title), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/packagezip', methods=['GET'])
+@login_required
+@roles_required(['admin', 'developer'])
+def download_zip_package():
+ package_name = request.args.get('package', None)
+ if not package_name:
+ return ('File not found', 404)
+ package_name = werkzeug.utils.secure_filename(package_name)
+ package = db.session.execute(select(Package).filter_by(active=True, name=package_name, type='zip')).scalar()
+ if package is None:
+ return ('File not found', 404)
+ if not current_user.has_role('admin'):
+ auth = db.session.execute(select(PackageAuth).filter_by(package_id=package.id, user_id=current_user.id)).scalar()
+ if auth is None:
+ return ('File not found', 404)
+ try:
+ file_info = get_info_from_file_number(package.upload, privileged=True)
+ except:
+ return ('File not found', 404)
+ filename = re.sub(r'\.', '-', package_name) + '.zip'
+ response = custom_send_file(file_info['path'] + '.zip', mimetype='application/zip', download_name=filename)
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(filename))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/updatingpackages', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def update_package_wait():
+ setup_translation()
+ if not (current_app.config['DEVELOPER_CAN_INSTALL'] or current_user.has_role('admin')):
+ return ('File not found', 404)
+ next_url = current_app.user_manager.make_safe_url_function(request.args.get('next', url_for('packages.update_package')))
+ my_csrf = generate_csrf()
+ initial_values = {
+ "daRestartAjax": url_for('main.restart_ajax'),
+ "daCsrf": my_csrf,
+ "daNoError": word("The package update did not report an error. The logs are below."),
+ "daErrorWithLog": word("The package update reported an error. The logs are below."),
+ "daUpdateError": word("There was an error updating the packages."),
+ "daGeneralError": word("There was an error."),
+ "daServerDidNotRespond": word("Server did not respond to request for update."),
+ "daUrlUpdatePackageAjax": url_for('develop.update_package_ajax')
+ }
+ script = f"""
+
+ {redis_script(initial_values)}"""
+ response = make_response(render_template('develop/update_package_wait.html', version_warning=None, bodyclass='daadminbody', extra_js=Markup(script), tab_title=word('Updating'), page_title=word('Updating'), next_page=next_url), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/update_package_ajax', methods=['POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def update_package_ajax():
+ if not (current_app.config['DEVELOPER_CAN_INSTALL'] or current_user.has_role('admin')):
+ return ('File not found', 404)
+ if 'taskwait' not in session or 'serverstarttime' not in session:
+ return jsonify(success=False)
+ setup_translation()
+ result = celery_app.AsyncResult(id=session['taskwait'])
+ if result.ready():
+ # if 'taskwait' in session:
+ # del session['taskwait']
+ the_result = result.get()
+ if the_result.__class__.__name__ == 'ReturnValue':
+ if the_result.ok:
+ # logmessage("update_package_ajax: success")
+ if (hasattr(the_result, 'restart') and not the_result.restart) or (START_TIME > session['serverstarttime'] and not reset_process_running()):
+ if len(the_result.logmessages) > 210000:
+ the_result.logmessages = the_result.logmessages[0:100000] + "\n\nTRUNCATED\n\n" + the_result.logmessages[-100000:]
+ return jsonify(success=True, status='finished', ok=the_result.ok, summary=summarize_results(the_result.results, the_result.logmessages))
+ return jsonify(success=True, status='waiting')
+ if hasattr(the_result, 'error_message'):
+ logmessage("update_package_ajax: failed return value is " + str(the_result.error_message))
+ return jsonify(success=True, status='failed', error_message=str(the_result.error_message))
+ if hasattr(the_result, 'results') and hasattr(the_result, 'logmessages'):
+ if len(the_result.logmessages) > 210000:
+ the_result.logmessages = the_result.logmessages[0:100000] + "\n\nTRUNCATED\n\n" + the_result.logmessages[-100000:]
+ return jsonify(success=True, status='failed', summary=summarize_results(the_result.results, the_result.logmessages))
+ return jsonify(success=True, status='failed', error_message=str("No error message. Result is " + str(the_result)))
+ logmessage("update_package_ajax: failed return value is a " + str(type(the_result)))
+ logmessage("update_package_ajax: failed return value is " + str(the_result))
+ return jsonify(success=True, status='failed', error_message=str(the_result))
+ return jsonify(success=True, status='waiting')
+
+
+@develop_bp.route('/createplaygroundpackage', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def create_playground_package():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ fix_package_folder()
+ playground_user = get_playground_user()
+ current_project = get_current_project()
+ form = CreatePlaygroundPackageForm(request.form)
+ current_package = request.args.get('package', None)
+ if current_package is not None:
+ current_package = werkzeug.utils.secure_filename(current_package)
+ do_pypi = request.args.get('pypi', False)
+ do_github = request.args.get('github', False)
+ if current_app.config['DEVELOPER_CAN_INSTALL'] or current_user.has_role('admin'):
+ do_install = request.args.get('install', False)
+ else:
+ do_install = False
+ branch = request.args.get('branch', None)
+ if branch is not None:
+ branch = branch.strip()
+ if branch in ('', 'None'):
+ branch = None
+ new_branch = request.args.get('new_branch', None)
+ if new_branch is not None and new_branch not in ('', 'None'):
+ branch = new_branch
+ sanitize_arguments(do_pypi, do_github, do_install, branch, new_branch)
+ if current_app.config['USE_GITHUB']:
+ github_auth = r.get('da:using_github:userid:' + str(current_user.id))
+ else:
+ github_auth = None
+ area = {}
+ area['playgroundpackages'] = SavedFile(playground_user.id, fix=True, section='playgroundpackages')
+ if os.path.isfile(os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + current_package)):
+ filename = os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + current_package)
+ info = {}
+ with open(filename, 'r', encoding='utf-8') as fp:
+ content = fp.read()
+ info = standardyaml.load(content, Loader=standardyaml.FullLoader)
+ else:
+ info = {}
+ if do_github:
+ if not current_app.config['USE_GITHUB']:
+ return ('File not found', 404)
+ if current_package is None:
+ logmessage('create_playground_package: package not specified')
+ return ('File not found', 404)
+ if not github_auth:
+ logmessage('create_playground_package: github button called when github auth not enabled.')
+ return ('File not found', 404)
+ github_auth = github_auth.decode()
+ if github_auth == '1':
+ github_auth_info = {'shared': True, 'orgs': True}
+ else:
+ github_auth_info = json.loads(github_auth)
+ github_package_name = 'docassemble-' + re.sub(r'^docassemble-', r'', current_package)
+ # github_package_name = re.sub(r'[^A-Za-z\_\-]', '', github_package_name)
+ if 'github_to_add' in session:
+ files_to_add = session['github_to_add']
+ del session['github_to_add']
+ else:
+ files_to_add = None
+ if github_package_name in ('docassemble-base', 'docassemble-webapp', 'docassemble-demo'):
+ return ('File not found', 404)
+ commit_message = request.args.get('commit_message', 'a commit')
+ storage = RedisCredStorage(oauth_app='github')
+ credentials = storage.get()
+ if not credentials or credentials.invalid:
+ state_string = random_string(16)
+ session['github_next'] = json.dumps({'state': state_string, 'path': 'create_playground_package', 'arguments': request.args})
+ flow = get_github_flow()
+ uri = flow.step1_get_authorize_url(state=state_string)
+ return redirect(uri)
+ http = credentials.authorize(httplib2.Http())
+ resp, content = http.request("https://api.github.com/user", "GET")
+ if int(resp['status']) == 200:
+ user_info = json.loads(content.decode())
+ github_user_name = user_info.get('login', None)
+ github_email = user_info.get('email', None)
+ else:
+ raise DAError("create_playground_package: could not get information about GitHub User")
+ if github_email is None:
+ resp, content = http.request("https://api.github.com/user/emails", "GET")
+ if int(resp['status']) == 200:
+ email_info = json.loads(content.decode())
+ for item in email_info:
+ if item.get('email', None) and item.get('visibility', None) != 'private':
+ github_email = item['email']
+ if github_user_name is None or github_email is None:
+ raise DAError("create_playground_package: login and/or email not present in user info from GitHub")
+ github_url_from_file = info.get('github_url', None)
+ found = False
+ found_strong = False
+ commit_repository = None
+ resp, content = http.request("https://api.github.com/repos/" + str(github_user_name) + "/" + github_package_name, "GET")
+ if int(resp['status']) == 200:
+ repo_info = json.loads(content.decode('utf-8', 'ignore'))
+ commit_repository = repo_info
+ found = True
+ if github_url_from_file is None or github_url_from_file in [repo_info['html_url'], repo_info['ssh_url']]:
+ found_strong = True
+ if found_strong is False and github_auth_info['shared']:
+ repositories = get_user_repositories(http)
+ for repo_info in repositories:
+ if repo_info['name'] != github_package_name or (commit_repository is not None and commit_repository.get('html_url', None) is not None and commit_repository['html_url'] == repo_info['html_url']) or (commit_repository is not None and commit_repository.get('ssh_url', None) is not None and commit_repository['ssh_url'] == repo_info['ssh_url']):
+ continue
+ if found and github_url_from_file is not None and github_url_from_file not in [repo_info['html_url'], repo_info['ssh_url']]:
+ break
+ commit_repository = repo_info
+ found = True
+ if github_url_from_file is None or github_url_from_file in [repo_info['html_url'], repo_info['ssh_url']]:
+ found_strong = True
+ break
+ if found_strong is False and github_auth_info['orgs']:
+ orgs_info = get_orgs_info(http)
+ for org_info in orgs_info:
+ resp, content = http.request("https://api.github.com/repos/" + str(org_info['login']) + "/" + github_package_name, "GET")
+ if int(resp['status']) == 200:
+ repo_info = json.loads(content.decode('utf-8', 'ignore'))
+ if found and github_url_from_file is not None and github_url_from_file not in [repo_info['html_url'], repo_info['ssh_url']]:
+ break
+ commit_repository = repo_info
+ break
+ file_list = {}
+ the_directory = directory_for(area['playgroundpackages'], current_project)
+ file_list['playgroundpackages'] = sorted([re.sub(r'^docassemble\.', r'', f) for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9]', f)])
+ the_choices = []
+ for file_option in file_list['playgroundpackages']:
+ the_choices.append((file_option, file_option))
+ form.name.choices = the_choices
+ if request.method == 'POST':
+ if form.validate():
+ current_package = form.name.data
+ # flash("form validated", 'success')
+ else:
+ the_error = ''
+ for error in form.name.errors:
+ the_error += str(error)
+ flash("form did not validate with " + str(form.name.data) + " " + str(the_error) + " among " + str(form.name.choices), 'error')
+ if current_package is not None:
+ pkgname = re.sub(r'^docassemble-', r'', current_package)
+ # if not user_can_edit_package(pkgname='docassemble.' + pkgname):
+ # flash(word('That package name is already in use by someone else. Please change the name.'), 'error')
+ # current_package = None
+ if current_package is not None and current_package not in file_list['playgroundpackages']:
+ flash(word('Sorry, that package name does not exist in the playground'), 'error')
+ current_package = None
+ if current_package is not None:
+ # section_sec = {'playgroundtemplate': 'template', 'playgroundstatic': 'static', 'playgroundsources': 'sources', 'playgroundmodules': 'modules'}
+ for sec in ('playground', 'playgroundtemplate', 'playgroundstatic', 'playgroundsources', 'playgroundmodules'):
+ area[sec] = SavedFile(playground_user.id, fix=True, section=sec)
+ the_directory = directory_for(area[sec], current_project)
+ if os.path.isdir(the_directory):
+ file_list[sec] = sorted([f for f in os.listdir(the_directory) if os.path.isfile(os.path.join(the_directory, f)) and re.search(r'^[A-Za-z0-9]', f)])
+ else:
+ file_list[sec] = []
+ if os.path.isfile(os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + current_package)):
+ filename = os.path.join(directory_for(area['playgroundpackages'], current_project), 'docassemble.' + current_package)
+ info = {}
+ with open(filename, 'r', encoding='utf-8') as fp:
+ content = fp.read()
+ info = standardyaml.load(content, Loader=standardyaml.FullLoader)
+ for field in ('dependencies', 'interview_files', 'template_files', 'module_files', 'static_files', 'sources_files'):
+ if field not in info:
+ info[field] = []
+ info['dependencies'] = list(x for x in map(lambda y: re.sub(r'[\>\<\=].*', '', y), info['dependencies']) if x not in ('docassemble', 'docassemble.base', 'docassemble.webapp'))
+ info['modtime'] = os.path.getmtime(filename)
+ author_info = {}
+ author_info['author name and email'] = name_of_user(playground_user, include_email=True)
+ author_info['author name'] = name_of_user(playground_user)
+ author_info['author email'] = playground_user.email
+ author_info['first name'] = playground_user.first_name
+ author_info['last name'] = playground_user.last_name
+ author_info['id'] = playground_user.id
+ if do_pypi:
+ if current_user.pypi_username is None or current_user.pypi_password is None or current_user.pypi_username == '' or current_user.pypi_password == '':
+ flash("Could not publish to PyPI because username and password were not defined")
+ return redirect(url_for('develop.playground_packages', project=current_project, file=current_package))
+ if playground_user.timezone:
+ the_timezone = playground_user.timezone
+ else:
+ the_timezone = get_default_timezone()
+ fix_ml_files(author_info['id'], current_project)
+ had_error, logmessages = publish_package(pkgname, info, author_info, current_project=current_project)
+ flash(logmessages, 'danger' if had_error else 'info')
+ if not do_install:
+ time.sleep(3.0)
+ return redirect(url_for('develop.playground_packages', project=current_project, file=current_package))
+ if do_github:
+ if commit_repository is not None:
+ resp, content = http.request("https://api.github.com/repos/" + commit_repository['full_name'] + "/commits?per_page=1", "GET")
+ if int(resp['status']) == 200:
+ commit_list = json.loads(content.decode('utf-8', 'ignore'))
+ if len(commit_list) == 0:
+ first_time = True
+ is_empty = True
+ else:
+ first_time = False
+ is_empty = False
+ else:
+ first_time = True
+ is_empty = True
+ else:
+ first_time = True
+ is_empty = False
+ headers = {'Content-Type': 'application/json'}
+ the_license = 'mit' if re.search(r'MIT', info.get('license', '')) else None
+ body = json.dumps({'name': github_package_name, 'description': info.get('description', None), 'homepage': info.get('url', None), 'license_template': the_license})
+ resp, content = http.request("https://api.github.com/user/repos", "POST", headers=headers, body=body)
+ if int(resp['status']) != 201:
+ raise DAError("create_playground_package: unable to create GitHub repository: status " + str(resp['status']) + " " + str(content))
+ resp, content = http.request("https://api.github.com/repos/" + str(github_user_name) + "/" + github_package_name, "GET")
+ if int(resp['status']) == 200:
+ commit_repository = json.loads(content.decode('utf-8', 'ignore'))
+ else:
+ raise DAError("create_playground_package: GitHub repository could not be found after creating it.")
+ if first_time:
+ logmessage("Not checking for stored commit code because no target repository exists")
+ pulled_already = False
+ else:
+ current_commit_file = os.path.join(directory_for(area['playgroundpackages'], current_project), '.' + github_package_name)
+ if os.path.isfile(current_commit_file):
+ with open(current_commit_file, 'r', encoding='utf-8') as fp:
+ commit_code = fp.read()
+ commit_code = commit_code.strip()
+ resp, content = http.request("https://api.github.com/repos/" + commit_repository['full_name'] + "/commits/" + commit_code, "GET")
+ if int(resp['status']) == 200:
+ logmessage("Stored commit code is valid")
+ pulled_already = True
+ else:
+ logmessage("Stored commit code is invalid")
+ pulled_already = False
+ else:
+ logmessage("Commit file not found")
+ pulled_already = False
+ directory = tempfile.mkdtemp(prefix='SavedFile')
+ (private_key_file, public_key_file) = get_ssh_keys(github_email)
+ os.chmod(private_key_file, stat.S_IRUSR | stat.S_IWUSR)
+ os.chmod(public_key_file, stat.S_IRUSR | stat.S_IWUSR)
+ ssh_script = tempfile.NamedTemporaryFile(mode='w', prefix="datemp", suffix='.sh', delete=False, encoding='utf-8')
+ ssh_script.write('# /bin/bash\n\nssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -i "' + str(private_key_file) + '" $1 $2 $3 $4 $5 $6')
+ ssh_script.close()
+ os.chmod(ssh_script.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
+ # git_prefix = "GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -i \"" + str(private_key_file) + "\"' "
+ git_prefix = "GIT_SSH=" + ssh_script.name + " "
+ git_env = dict(os.environ, GIT_SSH=ssh_script.name)
+ ssh_url = commit_repository.get('ssh_url', None)
+ # github_url = commit_repository.get('html_url', None)
+ commit_branch = commit_repository.get('default_branch', GITHUB_BRANCH)
+ if ssh_url is None:
+ raise DAError("create_playground_package: could not obtain ssh_url for package")
+ output = ''
+ # if branch:
+ # branch_option = '-b ' + str(branch) + ' '
+ # else:
+ # branch_option = '-b ' + commit_branch + ' '
+ tempbranch = 'playground' + random_string(5)
+ packagedir = os.path.join(directory, 'docassemble-' + str(pkgname))
+ the_user_name = str(playground_user.first_name) + " " + str(playground_user.last_name)
+ if the_user_name == ' ':
+ the_user_name = 'Anonymous User'
+ if is_empty:
+ os.makedirs(packagedir)
+ output += "Doing git init\n"
+ try:
+ output += subprocess.check_output(["git", "init"], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output
+ raise DAError("create_playground_package: error running git init. " + output) from err
+ with open(os.path.join(packagedir, 'README.md'), 'w', encoding='utf-8') as the_file:
+ the_file.write("")
+ if files_to_add is not None and '.gitignore' in files_to_add:
+ with open(os.path.join(packagedir, '.gitignore'), 'w', encoding='utf-8') as the_file:
+ the_file.write(DEFAULT_GITIGNORE)
+ output += "Doing git config user.email " + json.dumps(github_email) + "\n"
+ try:
+ output += subprocess.check_output(["git", "config", "user.email", json.dumps(github_email)], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git config user.email. " + output) from err
+ output += "Doing git config user.name " + json.dumps(the_user_name) + "\n"
+ try:
+ output += subprocess.check_output(["git", "config", "user.name", json.dumps(the_user_name)], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git config user.name. " + output) from err
+ output += "Doing git add README.md\n"
+ try:
+ output += subprocess.check_output(["git", "add", "README.md"], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git add README.md. " + output) from err
+ if files_to_add is not None and '.gitignore' in files_to_add:
+ output += "Doing git add .gitignore\n"
+ try:
+ output += subprocess.check_output(["git", "add", ".gitignore"], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git add .gitignore. " + output) from err
+ output += "Doing git commit -m \"first commit\"\n"
+ try:
+ output += subprocess.check_output(["git", "commit", "-m", "first commit"], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git commit -m \"first commit\". " + output) from err
+ output += "Doing git branch -M " + commit_branch + "\n"
+ try:
+ output += subprocess.check_output(["git", "branch", "-M", commit_branch], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git branch -M " + commit_branch + ". " + output) from err
+ output += "Doing git remote add origin " + ssh_url + "\n"
+ try:
+ output += subprocess.check_output(["git", "remote", "add", "origin", ssh_url], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git remote add origin. " + output) from err
+ output += "Doing " + git_prefix + "git push -u origin " + '"' + commit_branch + '"' + "\n"
+ try:
+ output += subprocess.check_output(["git", "push", "-u", "origin ", commit_branch], cwd=packagedir, stderr=subprocess.STDOUT, env=git_env).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running first git push. " + output) from err
+ else:
+ output += "Doing " + git_prefix + "git clone " + ssh_url + "\n"
+ try:
+ output += subprocess.check_output(["git", "clone", ssh_url], cwd=directory, stderr=subprocess.STDOUT, env=git_env).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git clone. " + output) from err
+ if not os.path.isdir(packagedir):
+ raise DAError("create_playground_package: package directory did not exist. " + output)
+ if pulled_already:
+ output += "Doing git checkout " + commit_code + "\n"
+ try:
+ output += subprocess.check_output(["git", "checkout", commit_code], cwd=packagedir, stderr=subprocess.STDOUT, env=git_env).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ # raise DAError("create_playground_package: error running git checkout. " + output)
+ if playground_user.timezone:
+ the_timezone = playground_user.timezone
+ else:
+ the_timezone = get_default_timezone()
+ fix_ml_files(author_info['id'], current_project)
+ if branch:
+ the_branch = branch
+ else:
+ the_branch = commit_branch
+ output += "Going to use " + the_branch + " as the branch.\n"
+ if not is_empty:
+ output += "Doing git config user.email " + json.dumps(github_email) + "\n"
+ try:
+ output += subprocess.check_output(["git", "config", "user.email", json.dumps(github_email)], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git config user.email. " + output) from err
+ output += "Doing git config user.name " + json.dumps(the_user_name) + "\n"
+ try:
+ output += subprocess.check_output(["git", "config", "user.name", json.dumps(the_user_name)], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git config user.email. " + output) from err
+ output += "Trying git checkout " + the_branch + "\n"
+ try:
+ output += subprocess.check_output(["git", "checkout", the_branch], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError:
+ output += the_branch + " is a new branch\n"
+ # force_branch_creation = True
+ branch = the_branch
+ output += "Doing git checkout -b " + tempbranch + "\n"
+ try:
+ output += subprocess.check_output(["git", "checkout", "-b", tempbranch], cwd=packagedir, stderr=subprocess.STDOUT, env=git_env).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git checkout. " + output) from err
+ output += "Writing files.\n"
+ make_package_dir(pkgname, info, author_info, directory=directory, current_project=current_project)
+ try:
+ if files_to_add is None:
+ output += "Doing git add .\n"
+ output += subprocess.check_output(["git", "add", "."], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ else:
+ output += "Doing git add " + (' '.join(files_to_add)) + "\n"
+ output += subprocess.check_output(["git", "add"] + files_to_add, cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output
+ raise DAError("create_playground_package: error running git add. " + output) from err
+ output += "Doing git status\n"
+ try:
+ output += subprocess.check_output(["git", "status"], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git status. " + output) from err
+ output += "Doing git commit -m " + json.dumps(str(commit_message)) + "\n"
+ try:
+ output += subprocess.check_output(["git", "commit", "-am", str(commit_message)], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git commit. " + output) from err
+ output += "Trying git checkout " + the_branch + "\n"
+ try:
+ output += subprocess.check_output(["git", "checkout", the_branch], cwd=packagedir, stderr=subprocess.STDOUT, env=git_env).decode()
+ branch_exists = True
+ except subprocess.CalledProcessError:
+ branch_exists = False
+ if not branch_exists:
+ output += "Doing git checkout -b " + the_branch + "\n"
+ try:
+ output += subprocess.check_output(["git", "checkout", "-b", the_branch], cwd=packagedir, stderr=subprocess.STDOUT, env=git_env).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git checkout -b " + the_branch + ". " + output) from err
+ else:
+ output += "Doing git merge --squash " + tempbranch + "\n"
+ try:
+ output += subprocess.check_output(["git", "merge", "--squash", tempbranch], cwd=packagedir, stderr=subprocess.STDOUT, env=git_env).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git merge --squash " + tempbranch + ". " + output) from err
+ output += "Doing git commit\n"
+ try:
+ output += subprocess.check_output(["git", "commit", "-am", str(commit_message)], cwd=packagedir, stderr=subprocess.STDOUT).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git commit -am " + str(commit_message) + ". " + output) from err
+ if branch:
+ output += "Doing " + git_prefix + "git push --set-upstream origin " + str(branch) + "\n"
+ try:
+ output += subprocess.check_output(["git", "push", "--set-upstream", "origin", str(branch)], cwd=packagedir, stderr=subprocess.STDOUT, env=git_env).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git push. " + output) from err
+ else:
+ output += "Doing " + git_prefix + "git push\n"
+ try:
+ output += subprocess.check_output(["git", "push"], cwd=packagedir, stderr=subprocess.STDOUT, env=git_env).decode()
+ except subprocess.CalledProcessError as err:
+ output += err.output.decode()
+ raise DAError("create_playground_package: error running git push. " + output) from err
+ logmessage(output)
+ flash(word("Pushed commit to GitHub.") + "
" + re.sub(r'[\n\r]+', '
', output), 'info')
+ time.sleep(3.0)
+ shutil.rmtree(directory)
+ the_args = {'project': current_project, 'pull': '1', 'github_url': ssh_url, 'show_message': '0'}
+ do_pypi_also = true_or_false(request.args.get('pypi_also', False))
+ if current_app.config['DEVELOPER_CAN_INSTALL'] or current_user.has_role('admin'):
+ do_install_also = true_or_false(request.args.get('install_also', False))
+ else:
+ do_install_also = False
+ if do_pypi_also or do_install_also:
+ the_args['file'] = current_package
+ if do_pypi_also:
+ the_args['pypi_also'] = '1'
+ if do_install_also:
+ the_args['install_also'] = '1'
+ if branch:
+ the_args['branch'] = branch
+ return redirect(url_for('develop.playground_packages', **the_args))
+ nice_name = 'docassemble-' + str(pkgname) + '.zip'
+ file_number = get_new_file_number(None, nice_name, None)
+ file_set_attributes(file_number, private=True, persistent=True, session=None, filename=None)
+ saved_file = SavedFile(file_number, extension='zip', fix=True, should_not_exist=True)
+ if playground_user.timezone:
+ the_timezone = playground_user.timezone
+ else:
+ the_timezone = get_default_timezone()
+ fix_ml_files(author_info['id'], current_project)
+ zip_file = make_package_zip(pkgname, info, author_info, the_timezone, current_project=current_project)
+ saved_file.copy_from(zip_file.name)
+ saved_file.finalize()
+ if do_install:
+ install_zip_package('docassemble.' + pkgname, file_number)
+ result = celery_app.signature('tasks.update_packages').apply_async(link=celery_app.signature('tasks.reset_server', kwargs={'run_create': should_run_create('docassemble.' + pkgname)}))
+ session['taskwait'] = result.id
+ session['serverstarttime'] = START_TIME
+ return redirect(url_for('develop.update_package_wait', next=url_for('develop.playground_packages', project=current_project, file=current_package)))
+ # return redirect(url_for('develop.playground_packages', file=current_package))
+ response = custom_send_file(saved_file.path, mimetype='application/zip', as_attachment=True, download_name=nice_name)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ response = make_response(render_template('develop/create_playground_package.html', current_project=current_project, version_warning=version_warning, bodyclass='daadminbody', form=form, current_package=current_package, package_names=file_list['playgroundpackages'], tab_title=word('Playground Packages'), page_title=word('Playground Packages')), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/createpackage', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def create_package():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ form = CreatePackageForm(request.form)
+ if request.method == 'POST' and form.validate():
+ pkgname = re.sub(r'^docassemble-', r'', form.name.data)
+ licensetext = """\
+The MIT License (MIT)
+
+"""
+ licensetext += 'Copyright (c) ' + str(datetime.datetime.now().year) + ' ' + str(name_of_user(current_user)) + """
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+"""
+ gitignore = daconfig.get('default gitignore', DEFAULT_GITIGNORE)
+ readme = '# docassemble.' + str(pkgname) + "\n\nA docassemble extension.\n\n## Author\n\n" + name_of_user(current_user, include_email=True) + "\n"
+ pyprojecttoml = tomli_w.dumps({'build-system': {'requires': ['setuptools>=80.9.0'], 'build-backend': 'setuptools.build_meta'}, 'project': {'name': f'docassemble.{pkgname}', 'version': '0.0.1', 'description': 'A docassemble extension.', 'readme': 'README.md', 'authors': [{'name': str(name_of_user(current_user)), 'email': str(current_user.email)}], 'license': 'MIT', 'license-files': ['LICENSE'], 'urls': {'Homepage': 'https://docassemble.org'}}, 'tool': {'setuptools': {'packages': {'find': {'where': ['.']}}}}})
+ manifestin = f"""\
+include README.md
+graft docassemble/{pkgname}/data
+recursive-exclude * *.egg-info
+recursive-exclude .git *
+recursive-exclude venv *
+recursive-exclude .github *
+recursive-exclude .pytest_cache *
+recursive-exclude .vscode *
+recursive-exclude build *
+recursive-exclude dist *
+recursive-exclude * __pycache__
+recursive-exclude * *.pyc
+recursive-exclude * *.pyo
+recursive-exclude * *.orig
+recursive-exclude * *~
+recursive-exclude * *.bak
+recursive-exclude * *.swp
+"""
+ setupcfg = """\
+[metadata]
+long_description = file: README.md
+"""
+ setuppy = """\
+import os
+import sys
+from setuptools import setup, find_namespace_packages
+from fnmatch import fnmatchcase
+from distutils2.util import convert_path
+
+standard_exclude = ('*.pyc', '*~', '.*', '*.bak', '*.swp*')
+standard_exclude_directories = ('.*', 'CVS', '_darcs', os.path.join('.', 'build'), os.path.join('.', 'dist'), 'EGG-INFO', '*.egg-info')
+def find_package_data(where='.', package='', exclude=standard_exclude, exclude_directories=standard_exclude_directories):
+ out = {}
+ stack = [(convert_path(where), '', package)]
+ while stack:
+ where, prefix, package = stack.pop(0)
+ for name in os.listdir(where):
+ fn = os.path.join(where, name)
+ if os.path.isdir(fn):
+ bad_name = False
+ for pattern in exclude_directories:
+ if (fnmatchcase(name, pattern)
+ or fn.lower() == pattern.lower()):
+ bad_name = True
+ break
+ if bad_name:
+ continue
+ if os.path.isfile(os.path.join(fn, '__init__.py')):
+ if not package:
+ new_package = name
+ else:
+ new_package = package + '.' + name
+ stack.append((fn, '', new_package))
+ else:
+ stack.append((fn, prefix + name + os.path.sep, package))
+ else:
+ bad_name = False
+ for pattern in exclude:
+ if (fnmatchcase(name, pattern)
+ or fn.lower() == pattern.lower()):
+ bad_name = True
+ break
+ if bad_name:
+ continue
+ out.setdefault(package, []).append(prefix+name)
+ return out
+
+"""
+ setuppy += "setup(name='docassemble." + str(pkgname) + "',\n" + """\
+ version='0.0.1',
+ description=('A docassemble extension.'),
+ long_description=""" + repr(readme) + """,
+ long_description_content_type='text/markdown',
+ author=""" + repr(str(name_of_user(current_user))) + """,
+ author_email=""" + repr(str(current_user.email)) + """,
+ license='MIT',
+ url='https://docassemble.org',
+ packages=find_namespace_packages(),
+ zip_safe = False,
+ package_data=find_package_data(where=os.path.join('docassemble', '""" + str(pkgname) + """', ''), package='docassemble.""" + str(pkgname) + """'),
+ )
+
+"""
+ questionfiletext = """\
+---
+metadata:
+ title: I am the title of the application
+ short title: Mobile title
+ description: |
+ Insert description of question file here.
+ authors:
+ - name: """ + str(current_user.first_name) + " " + str(current_user.last_name) + """
+ organization: """ + str(current_user.organization) + """
+ revision_date: """ + formatted_current_date() + """
+---
+mandatory: True
+code: |
+ user_done
+---
+question: |
+ % if user_doing_well:
+ Good to hear it!
+ % else:
+ Sorry to hear that!
+ % endif
+sets: user_done
+buttons:
+ - Exit: exit
+ - Restart: restart
+---
+question: Are you doing well today?
+yesno: user_doing_well
+...
+"""
+ templatereadme = """\
+# Template directory
+
+If you want to use templates for document assembly, put them in this directory.
+"""
+ staticreadme = """\
+# Static file directory
+
+If you want to make files available in the web app, put them in
+this directory.
+"""
+ sourcesreadme = """\
+# Sources directory
+
+This directory is used to store word translation files,
+machine learning training files, and other sources of data.
+"""
+ objectfile = """\
+# This is a Python module in which you can write your own Python code,
+# if you want to.
+#
+# Include this module in a docassemble interview by writing:
+# ---
+# modules:
+# - docassemble.""" + pkgname + """.objects
+# ---
+#
+# Then you can do things like:
+# ---
+# objects:
+# - favorite_fruit: Fruit
+# ---
+# mandatory: True
+# question: |
+# When I eat some ${ favorite_fruit.name },
+# I think, "${ favorite_fruit.eat() }"
+# ---
+# question: What is the best fruit?
+# fields:
+# - Fruit Name: favorite_fruit.name
+# ---
+from docassemble.base.util import DAObject
+
+
+class Fruit(DAObject):
+
+ def eat(self):
+ return "Yum, that " + self.name + " was good!"
+"""
+ directory = tempfile.mkdtemp(prefix='SavedFile')
+ packagedir = os.path.join(directory, 'docassemble-' + str(pkgname))
+ questionsdir = os.path.join(packagedir, 'docassemble', str(pkgname), 'data', 'questions')
+ templatesdir = os.path.join(packagedir, 'docassemble', str(pkgname), 'data', 'templates')
+ staticdir = os.path.join(packagedir, 'docassemble', str(pkgname), 'data', 'static')
+ sourcesdir = os.path.join(packagedir, 'docassemble', str(pkgname), 'data', 'sources')
+ os.makedirs(questionsdir, exist_ok=True)
+ os.makedirs(templatesdir, exist_ok=True)
+ os.makedirs(staticdir, exist_ok=True)
+ os.makedirs(sourcesdir, exist_ok=True)
+ with open(os.path.join(packagedir, '.gitignore'), 'w', encoding='utf-8') as the_file:
+ the_file.write(gitignore)
+ with open(os.path.join(packagedir, 'README.md'), 'w', encoding='utf-8') as the_file:
+ the_file.write(readme)
+ with open(os.path.join(packagedir, 'LICENSE'), 'w', encoding='utf-8') as the_file:
+ the_file.write(licensetext)
+ with open(os.path.join(packagedir, 'setup.py'), 'w', encoding='utf-8') as the_file:
+ the_file.write(setuppy)
+ with open(os.path.join(packagedir, 'setup.cfg'), 'w', encoding='utf-8') as the_file:
+ the_file.write(setupcfg)
+ with open(os.path.join(packagedir, 'MANIFEST.in'), 'w', encoding='utf-8') as the_file:
+ the_file.write(manifestin)
+ with open(os.path.join(packagedir, 'pyproject.toml'), 'w', encoding='utf-8') as the_file:
+ the_file.write(pyprojecttoml)
+ with open(os.path.join(packagedir, 'docassemble', pkgname, '__init__.py'), 'w', encoding='utf-8') as the_file:
+ the_file.write('__version__ = "0.0.1"')
+ with open(os.path.join(packagedir, 'docassemble', pkgname, 'objects.py'), 'w', encoding='utf-8') as the_file:
+ the_file.write(objectfile)
+ with open(os.path.join(templatesdir, 'README.md'), 'w', encoding='utf-8') as the_file:
+ the_file.write(templatereadme)
+ with open(os.path.join(staticdir, 'README.md'), 'w', encoding='utf-8') as the_file:
+ the_file.write(staticreadme)
+ with open(os.path.join(sourcesdir, 'README.md'), 'w', encoding='utf-8') as the_file:
+ the_file.write(sourcesreadme)
+ with open(os.path.join(questionsdir, 'questions.yml'), 'w', encoding='utf-8') as the_file:
+ the_file.write(questionfiletext)
+ nice_name = 'docassemble-' + str(pkgname) + '.zip'
+ file_number = get_new_file_number(None, nice_name, None)
+ file_set_attributes(file_number, private=True, persistent=True, session=None, filename=None)
+ saved_file = SavedFile(file_number, extension='zip', fix=True, should_not_exist=True)
+ zf = zipfile.ZipFile(saved_file.path, compression=zipfile.ZIP_DEFLATED, mode='w')
+ trimlength = len(directory) + 1
+ if current_user.timezone:
+ the_timezone = zoneinfo.ZoneInfo(current_user.timezone)
+ else:
+ the_timezone = zoneinfo.ZoneInfo(get_default_timezone())
+ for root, dirs, files in os.walk(packagedir): # pylint: disable=unused-variable
+ for the_file in files:
+ thefilename = os.path.join(root, the_file)
+ info = zipfile.ZipInfo(thefilename[trimlength:])
+ info.date_time = datetime.datetime.fromtimestamp(os.path.getmtime(thefilename), datetime.timezone.utc).astimezone(the_timezone).timetuple()
+ info.compress_type = zipfile.ZIP_DEFLATED
+ info.external_attr = 0o644 << 16
+ with open(thefilename, 'rb') as fp:
+ zf.writestr(info, fp.read())
+ # zf.write(thefilename, thefilename[trimlength:])
+ zf.close()
+ saved_file.save()
+ saved_file.finalize()
+ shutil.rmtree(directory)
+ response = custom_send_file(saved_file.path, mimetype='application/zip', as_attachment=True, download_name=nice_name)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ flash(word("Package created"), 'success')
+ return response
+ response = make_response(render_template('develop/create_package.html', version_warning=version_warning, bodyclass='daadminbody', form=form, tab_title=word('Create Package'), page_title=word('Create Package')), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+@develop_bp.route('/playgroundbundle.js', methods=['GET'])
+def playground_js_bundle():
+ base_path = Path(importlib.resources.files('docassemble.webapp'), 'static')
+ output = ''
+ for parts in [['areyousure', 'jquery.are-you-sure.js'], ['bootstrap-fileinput', 'js', 'plugins', 'buffer.js'], ['bootstrap-fileinput', 'js', 'plugins', 'filetype.js'], ['bootstrap-fileinput', 'js', 'plugins', 'piexif.js'], ['bootstrap-fileinput', 'js', 'plugins', 'sortable.js'], ['bootstrap-fileinput', 'js', 'fileinput.js'], ['app', 'cm6.js'], ['app', 'playground.js']]:
+ with open(os.path.join(base_path, *parts), encoding='utf-8') as fp:
+ output += fp.read()
+ output += "\n"
+ return Response(output, mimetype='application/javascript')
+
+
+@develop_bp.route('/reqdev', methods=['GET', 'POST'])
+@login_required
+def request_developer():
+ setup_translation()
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ return ('File not found', 404)
+ form = RequestDeveloperForm(request.form)
+ recipients = []
+ if request.method == 'POST':
+ for user in db.session.execute(select(UserModel.id, UserModel.email).join(UserRoles, UserModel.id == UserRoles.user_id).join(Role, UserRoles.role_id == Role.id).where(and_(UserModel.active == True, Role.name == 'admin'))): # noqa: E712 # pylint: disable=singleton-comparison
+ if user.email not in recipients:
+ recipients.append(user.email)
+ body = "User " + str(current_user.email) + " (" + str(current_user.id) + ") has requested developer privileges.\n\n"
+ if form.reason.data:
+ body += "Reason given: " + str(form.reason.data) + "\n\n"
+ body += "Go to " + url_for('users.edit_user_profile_page', user_id=current_user.id, _external=True) + " to change the user's privileges."
+ msg = Message("Request for developer account from " + str(current_user.email), recipients=recipients, body=body)
+ if len(recipients) == 0:
+ flash(word('No administrators could be found.'), 'error')
+ else:
+ try:
+ da_send_mail(msg, None)
+ flash(word('Your request was submitted.'), 'success')
+ except:
+ flash(word('We were unable to submit your request.'), 'error')
+ return redirect(url_for('user.profile'))
+ return render_template('users/request_developer.html', version_warning=None, bodyclass='daadminbody', tab_title=word("Developer Access"), page_title=word("Developer Access"), form=form)
+
+
+@develop_bp.route('/utilities', methods=['GET', 'POST'])
+@login_required
+@roles_required(['admin', 'developer'])
+def utilities():
+ setup_translation()
+ form = Utilities(request.form)
+ fields_output = None
+ word_box = None
+ uses_null = False
+ file_type = None
+ if request.method == 'GET' and needs_to_change_password():
+ return redirect(url_for('user.change_password', next=url_for('utilities')))
+ if request.method == 'POST':
+ if 'language' in request.form:
+ language = request.form['language']
+ result = {}
+ result[language] = {}
+ existing = word_collection.get(language, {})
+ if 'api key' in daconfig['google'] and daconfig['google']['api key']:
+ import googleapiclient.discovery # pylint: disable=import-outside-toplevel
+ try:
+ service = googleapiclient.discovery.build('translate', 'v2',
+ developerKey=daconfig['google']['api key'])
+ use_google_translate = True
+ except:
+ logmessage("utilities: attempt to call Google Translate failed")
+ use_google_translate = False
+ else:
+ use_google_translate = False
+ service = None
+ words_to_translate = []
+ for the_word in base_words:
+ if the_word in existing and existing[the_word] is not None:
+ result[language][the_word] = existing[the_word]
+ continue
+ words_to_translate.append(the_word)
+ chunk_limit = daconfig.get('google translate words at a time', 20)
+ chunks = []
+ interim_list = []
+ while len(words_to_translate) > 0:
+ the_word = words_to_translate.pop(0)
+ interim_list.append(the_word)
+ if len(interim_list) >= chunk_limit:
+ chunks.append(interim_list)
+ interim_list = []
+ if len(interim_list) > 0:
+ chunks.append(interim_list)
+ for chunk in chunks:
+ if use_google_translate:
+ try:
+ resp = service.translations().list( # pylint: disable=no-member
+ source='en',
+ target=language,
+ q=chunk
+ ).execute()
+ except BaseException as errstr:
+ logmessage("utilities: translation failed: " + str(errstr))
+ resp = None
+ if isinstance(resp, dict) and 'translations' in resp and isinstance(resp['translations'], list) and len(resp['translations']) == len(chunk):
+ for the_index, the_chunk in enumerate(chunk):
+ if isinstance(resp['translations'][the_index], dict) and 'translatedText' in resp['translations'][the_index]:
+ result[language][the_chunk] = re.sub(r''', r"'", str(resp['translations'][the_index]['translatedText']))
+ else:
+ result[language][the_chunk] = 'XYZNULLXYZ'
+ uses_null = True
+ else:
+ for the_word in chunk:
+ result[language][the_word] = 'XYZNULLXYZ'
+ uses_null = True
+ else:
+ for the_word in chunk:
+ result[language][the_word] = 'XYZNULLXYZ'
+ uses_null = True
+ if form.systemfiletype.data == 'YAML':
+ word_box = altyamlstring.dump_to_string(result)
+ word_box = re.sub(r'"XYZNULLXYZ"', r'null', word_box)
+ elif form.systemfiletype.data == 'XLSX':
+ temp_file = tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False)
+ xlsx_filename = language + "-words.xlsx"
+ workbook = xlsxwriter.Workbook(temp_file.name)
+ worksheet = workbook.add_worksheet()
+ bold = workbook.add_format({'bold': 1, 'num_format': '@'})
+ text = workbook.add_format({'num_format': '@'})
+ text.set_align('top')
+ wrapping = workbook.add_format({'num_format': '@'})
+ wrapping.set_align('top')
+ wrapping.set_text_wrap()
+ # wrapping.set_locked(False)
+ numb = workbook.add_format()
+ numb.set_align('top')
+ worksheet.write('A1', 'orig_lang', bold)
+ worksheet.write('B1', 'tr_lang', bold)
+ worksheet.write('C1', 'orig_text', bold)
+ worksheet.write('D1', 'tr_text', bold)
+ worksheet.set_column(0, 0, 10)
+ worksheet.set_column(1, 1, 10)
+ worksheet.set_column(2, 2, 55)
+ worksheet.set_column(3, 3, 55)
+ row = 1
+ for key, val in result[language].items():
+ worksheet.write_string(row, 0, 'en', text)
+ worksheet.write_string(row, 1, language, text)
+ worksheet.write_string(row, 2, key, wrapping)
+ worksheet.write_string(row, 3, val, wrapping)
+ row += 1
+ workbook.close()
+ response = custom_send_file(temp_file.name, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=xlsx_filename)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ elif form.systemfiletype.data == 'XLIFF 1.2':
+ temp_file = tempfile.NamedTemporaryFile(suffix='.xlf', delete=False)
+ xliff_filename = language + "-words.xlf"
+ xliff = ET.Element('xliff')
+ xliff.set('xmlns', 'urn:oasis:names:tc:xliff:document:1.2')
+ xliff.set('version', '1.2')
+ the_file = ET.SubElement(xliff, 'file')
+ the_file.set('source-language', 'en')
+ the_file.set('target-language', language)
+ the_file.set('datatype', 'plaintext')
+ the_file.set('original', 'self')
+ the_file.set('id', 'f1')
+ the_file.set('xml:space', 'preserve')
+ body = ET.SubElement(the_file, 'body')
+ indexno = 1
+ for key, val in result[language].items():
+ trans_unit = ET.SubElement(body, 'trans-unit')
+ trans_unit.set('id', str(indexno))
+ trans_unit.set('xml:space', 'preserve')
+ source = ET.SubElement(trans_unit, 'source')
+ source.set('xml:space', 'preserve')
+ target = ET.SubElement(trans_unit, 'target')
+ target.set('xml:space', 'preserve')
+ source.text = key
+ target.text = val
+ indexno += 1
+ temp_file.write(ET.tostring(xliff))
+ temp_file.close()
+ response = custom_send_file(temp_file.name, mimetype='application/xml', as_attachment=True, download_name=xliff_filename)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ elif form.systemfiletype.data == 'XLIFF 2.0':
+ temp_file = tempfile.NamedTemporaryFile(suffix='.xlf', delete=False)
+ xliff_filename = language + "-words.xlf"
+ xliff = ET.Element('xliff')
+ xliff.set('xmlns', 'urn:oasis:names:tc:xliff:document:2.0')
+ xliff.set('version', '2.0')
+ xliff.set('srcLang', 'en')
+ xliff.set('trgLang', language)
+ the_file = ET.SubElement(xliff, 'file')
+ the_file.set('id', 'f1')
+ the_file.set('original', 'self')
+ the_file.set('xml:space', 'preserve')
+ unit = ET.SubElement(the_file, 'unit')
+ unit.set('id', "docassemble_phrases")
+ indexno = 1
+ for key, val in result[language].items():
+ segment = ET.SubElement(unit, 'segment')
+ segment.set('id', str(indexno))
+ segment.set('xml:space', 'preserve')
+ source = ET.SubElement(segment, 'source')
+ source.set('xml:space', 'preserve')
+ target = ET.SubElement(segment, 'target')
+ target.set('xml:space', 'preserve')
+ source.text = key
+ target.text = val
+ indexno += 1
+ temp_file.write(ET.tostring(xliff))
+ temp_file.close()
+ response = custom_send_file(temp_file.name, mimetype='application/xml', as_attachment=True, download_name=xliff_filename)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ if 'pdfdocxfile' in request.files and request.files['pdfdocxfile'].filename:
+ filename = secure_filename(request.files['pdfdocxfile'].filename)
+ extension, mimetype = get_ext_and_mimetype(filename) # pylint: disable=unused-variable
+ if mimetype == 'application/pdf':
+ file_type = 'pdf'
+ pdf_file = tempfile.NamedTemporaryFile(mode="wb", suffix=".pdf", delete=True)
+ the_file = request.files['pdfdocxfile']
+ the_file.save(pdf_file.name)
+ try:
+ fields_output = read_fields(pdf_file.name, the_file.filename, 'pdf', 'yaml')
+ except BaseException as err:
+ fields_output = str(err)
+ pdf_file.close()
+ elif mimetype == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
+ file_type = 'docx'
+ docx_file = tempfile.NamedTemporaryFile(mode="wb", suffix=".docx", delete=True)
+ the_file = request.files['pdfdocxfile']
+ the_file.save(docx_file.name)
+ try:
+ fields_output = read_fields(docx_file.name, the_file.filename, 'docx', 'yaml')
+ except BaseException as err:
+ fields_output = str(err)
+ docx_file.close()
+ if form.officeaddin_submit.data:
+ resp = make_response(render_template('develop/officemanifest.xml', office_app_version=form.officeaddin_version.data, guid=str(uuid.uuid4())))
+ resp.headers['Content-type'] = 'text/xml; charset=utf-8'
+ resp.headers['Content-Disposition'] = 'attachment; filename="manifest.xml"'
+ resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return resp
+ form.systemfiletype.choices = [('YAML', 'YAML'), ('XLSX', 'XLSX'), ('XLIFF 1.2', 'XLIFF 1.2'), ('XLIFF 2.0', 'XLIFF 2.0')]
+ form.systemfiletype.data = 'YAML'
+ form.filetype.choices = [('XLSX', 'XLSX'), ('XLIFF 1.2', 'XLIFF 1.2'), ('XLIFF 2.0', 'XLIFF 2.0')]
+ form.filetype.data = 'XLSX'
+ response = make_response(render_template('develop/utilities.html', version_warning=version_warning, bodyclass='daadminbody', tab_title=word("Utilities"), page_title=word("Utilities"), form=form, fields=fields_output, word_box=word_box, uses_null=uses_null, file_type=file_type, interview_placeholder=word("E.g., docassemble.demo:data/questions/questions.yml"), language_placeholder=word("E.g., es, fr, it")), 200)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
diff --git a/docassemble_webapp/docassemble/webapp/emailserver/__init__.py b/docassemble_webapp/docassemble/webapp/emailserver/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/docassemble_webapp/docassemble/webapp/emailserver/helpers.py b/docassemble_webapp/docassemble/webapp/emailserver/helpers.py
new file mode 100644
index 000000000..c793b1d22
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/emailserver/helpers.py
@@ -0,0 +1,225 @@
+import json
+from flask import session
+from flask_login import current_user
+from sqlalchemy import select, delete
+from docassemble.base.error import DAError
+from docassemble.base.functions import get_uid
+from docassemble.base.generate_key import random_lower_string
+from docassemble.base.thread_context import this_thread
+from docassemble.base.util import (
+ DAEmailRecipient,
+ DAFile,
+ DAEmail,
+ DAEmailRecipientList,
+ DAFileList,
+)
+from docassemble.webapp.extensions import db
+from docassemble.webapp.files.savedfile import SavedFile
+from docassemble.webapp.hooks.impl import hookimpl
+from docassemble.webapp.main.models import Uploads
+from docassemble.webapp.users.helpers import get_user_object
+from docassemble.webapp.users.models import UserModel
+from docassemble.webapp.utils.filenames import get_ext_and_mimetype
+from docassemble.webapp.utils.helpers import process_file
+from .models import Email, Shortener, EmailAttachment
+
+@hookimpl
+def get_short_code(kwargs):
+ key = kwargs.get('key', None)
+ the_index = kwargs.get('index', None)
+ if key is None and the_index is not None:
+ raise DAError("get_short_code: if you provide an index you must provide a key")
+ yaml_filename = kwargs.get('i', this_thread.current_info.get('yaml_filename', None))
+ uid = kwargs.get('uid', get_uid())
+ if 'user_id' in kwargs:
+ user_id = kwargs['user_id']
+ temp_user_id = None
+ elif 'temp_user_id' in kwargs:
+ user_id = None
+ temp_user_id = kwargs['temp_user_id']
+ elif current_user.is_anonymous:
+ user_id = None
+ temp_user_id = session.get('tempuser', None)
+ else:
+ user_id = current_user.id
+ temp_user_id = None
+ short_code = None
+ for record in db.session.execute(select(Shortener.short).filter_by(filename=yaml_filename, uid=uid, user_id=user_id, temp_user_id=temp_user_id, key=key, index=the_index)):
+ short_code = record.short
+ if short_code is not None:
+ return short_code
+ counter = 0
+ new_record = None
+ while counter < 20:
+ existing_id = None
+ new_short = random_lower_string(6)
+ for record in db.session.execute(select(Shortener.id).filter_by(short=new_short)):
+ existing_id = record.id
+ if existing_id is None:
+ new_record = Shortener(filename=yaml_filename, uid=uid, user_id=user_id, temp_user_id=temp_user_id, short=new_short, key=key, index=the_index)
+ db.session.add(new_record)
+ db.session.commit()
+ break
+ counter += 1
+ if new_record is None:
+ raise SystemError("Failed to generate unique short code")
+ return new_short
+
+
+@hookimpl
+def retrieve_email(email_id):
+ if not isinstance(email_id, int):
+ raise DAError("email_id not provided")
+ email = db.session.execute(select(Email).filter_by(id=email_id)).scalar()
+ if email is None:
+ raise DAError("E-mail did not exist")
+ short_record = db.session.execute(select(Shortener).filter_by(short=email.short)).scalar()
+ if short_record is not None and short_record.user_id is not None:
+ user = db.session.execute(select(UserModel).options(db.joinedload(UserModel.roles)).filter_by(id=short_record.user_id, active=True)).scalar()
+ else:
+ user = None
+ if short_record is None:
+ raise DAError("Short code did not exist")
+ return get_email_obj(email, short_record, user)
+
+
+class AddressEmail:
+
+ def __str__(self):
+ return str(self.address)
+
+
+@hookimpl
+def retrieve_emails(kwargs):
+ key = kwargs.get('key', None)
+ the_index = kwargs.get('index', None)
+ if key is None and the_index is not None:
+ raise DAError("retrieve_emails: if you provide an index you must provide a key")
+ yaml_filename = kwargs.get('i', this_thread.current_info.get('yaml_filename', None))
+ uid = kwargs.get('uid', get_uid())
+ if 'user_id' in kwargs:
+ user_id = kwargs['user_id']
+ temp_user_id = None
+ elif 'temp_user_id' in kwargs:
+ user_id = None
+ temp_user_id = kwargs['temp_user_id']
+ elif current_user.is_anonymous:
+ user_id = None
+ temp_user_id = session.get('tempuser', None)
+ else:
+ user_id = current_user.id
+ temp_user_id = None
+ user_cache = {}
+ results = []
+ if key is None:
+ the_query = db.session.execute(select(Shortener).filter_by(filename=yaml_filename, uid=uid, user_id=user_id, temp_user_id=temp_user_id).order_by(Shortener.modtime)).scalars()
+ else:
+ if the_index is None:
+ the_query = db.session.execute(select(Shortener).filter_by(filename=yaml_filename, uid=uid, user_id=user_id, temp_user_id=temp_user_id, key=key).order_by(Shortener.modtime)).scalars()
+ else:
+ the_query = db.session.execute(select(Shortener).filter_by(filename=yaml_filename, uid=uid, user_id=user_id, temp_user_id=temp_user_id, key=key, index=the_index).order_by(Shortener.modtime)).scalars()
+ for record in the_query:
+ result_for_short = AddressEmail()
+ result_for_short.address = record.short
+ result_for_short.key = record.key
+ result_for_short.index = record.index
+ result_for_short.emails = []
+ if record.user_id is not None:
+ if record.user_id in user_cache:
+ user = user_cache[record.user_id]
+ else:
+ user = get_user_object(record.user_id)
+ user_cache[record.user_id] = user
+ result_for_short.owner = user.email
+ else:
+ user = None
+ result_for_short.owner = None
+ for email in db.session.execute(select(Email).filter_by(short=record.short).order_by(Email.datetime_received)).scalars():
+ result_for_short.emails.append(get_email_obj(email, record, user))
+ results.append(result_for_short)
+ return results
+
+
+def get_email_obj(email, short_record, user):
+ email_obj = DAEmail(short=email.short)
+ email_obj.key = short_record.key
+ email_obj.index = short_record.index
+ email_obj.initializeAttribute('to_address', DAEmailRecipientList, json.loads(email.to_addr), gathered=True)
+ email_obj.initializeAttribute('cc_address', DAEmailRecipientList, json.loads(email.cc_addr), gathered=True)
+ email_obj.initializeAttribute('from_address', DAEmailRecipient, **json.loads(email.from_addr))
+ email_obj.initializeAttribute('reply_to', DAEmailRecipient, **json.loads(email.reply_to_addr))
+ email_obj.initializeAttribute('return_path', DAEmailRecipient, **json.loads(email.return_path_addr))
+ email_obj.subject = email.subject
+ email_obj.datetime_message = email.datetime_message
+ email_obj.datetime_received = email.datetime_received
+ email_obj.initializeAttribute('attachment', DAFileList, gathered=True)
+ if user is None:
+ email_obj.address_owner = None
+ else:
+ email_obj.address_owner = user.email
+ for attachment_record in db.session.execute(select(EmailAttachment).filter_by(email_id=email.id).order_by(EmailAttachment.index)).scalars():
+ # logmessage("Attachment record is " + str(attachment_record.id))
+ upload = db.session.execute(select(Uploads).filter_by(indexno=attachment_record.upload)).scalar()
+ if upload is None:
+ continue
+ # logmessage("Filename is " + upload.filename)
+ saved_file_att = SavedFile(attachment_record.upload, extension=attachment_record.extension, fix=True)
+ process_file(saved_file_att, saved_file_att.path, attachment_record.content_type, attachment_record.extension, initial=False)
+ extension, mimetype = get_ext_and_mimetype(upload.filename)
+ if upload.filename == 'headers.json':
+ # logmessage("Processing headers")
+ email_obj.initializeAttribute('headers', DAFile, mimetype=mimetype, extension=extension, number=attachment_record.upload)
+ elif upload.filename == 'attachment.txt' and attachment_record.index < 3:
+ # logmessage("Processing body text")
+ email_obj.initializeAttribute('body_text', DAFile, mimetype=mimetype, extension=extension, number=attachment_record.upload)
+ elif upload.filename == 'attachment.html' and attachment_record.index < 3:
+ email_obj.initializeAttribute('body_html', DAFile, mimetype=mimetype, extension=extension, number=attachment_record.upload)
+ else:
+ email_obj.attachment.appendObject(DAFile, mimetype=mimetype, extension=extension, number=attachment_record.upload)
+ if not hasattr(email_obj, 'headers'):
+ email_obj.headers = None
+ if not hasattr(email_obj, 'body_text'):
+ email_obj.body_text = None
+ if not hasattr(email_obj, 'body_html'):
+ email_obj.body_html = None
+ return email_obj
+
+@hookimpl
+def manage_email_server_objects(mode, kwargs):
+ if mode == 0:
+ temp_user_id = kwargs['temp_user_id']
+ files_to_delete = []
+ for short_code_item in db.session.execute(select(Shortener).filter_by(temp_user_id=temp_user_id)).scalars():
+ for email in db.session.execute(select(Email).filter_by(short=short_code_item.short)).scalars():
+ for attachment in db.session.execute(select(EmailAttachment).filter_by(email_id=email.id)).scalars():
+ files_to_delete.append(attachment.upload)
+ for file_number in files_to_delete:
+ the_file = SavedFile(file_number)
+ the_file.delete()
+ db.session.execute(delete(Shortener).where(Shortener.temp_user_id == temp_user_id))
+ db.session.commit()
+ if mode == 1:
+ user_id = kwargs['user_id']
+ files_to_delete = []
+ for short_code_item in db.session.execute(select(Shortener).filter_by(user_id=user_id)).scalars():
+ for email in db.session.execute(select(Email).filter_by(short=short_code_item.short)).scalars():
+ for attachment in db.session.execute(select(EmailAttachment).filter_by(email_id=email.id)).scalars():
+ files_to_delete.append(attachment.upload)
+ for file_number in files_to_delete:
+ the_file = SavedFile(file_number)
+ the_file.delete()
+ db.session.execute(delete(Shortener).where(Shortener.user_id == user_id))
+ db.session.commit()
+ if mode == 2:
+ user_code = kwargs['user_code']
+ filename = kwargs['user_code']
+ files_to_delete = []
+ for short_code_item in db.session.execute(select(Shortener).filter_by(uid=user_code, filename=filename)).scalars():
+ for email in db.session.execute(select(Email).filter_by(short=short_code_item.short)).scalars():
+ for attachment in db.session.execute(select(EmailAttachment).filter_by(email_id=email.id)).scalars():
+ files_to_delete.append(attachment.upload)
+ for file_number in files_to_delete:
+ the_file = SavedFile(file_number)
+ the_file.delete()
+ db.session.execute(delete(Shortener).filter_by(uid=user_code, filename=filename))
+ db.session.commit()
diff --git a/docassemble_webapp/docassemble/webapp/emailserver/models.py b/docassemble_webapp/docassemble/webapp/emailserver/models.py
new file mode 100644
index 000000000..f09758df5
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/emailserver/models.py
@@ -0,0 +1,58 @@
+from datetime import datetime
+from typing import Optional
+from sqlalchemy.orm import Mapped, mapped_column
+from sqlalchemy import Integer, String, Text, ForeignKey, DateTime, func
+from docassemble.webapp.database import dbtableprefix
+from docassemble.webapp.db_base import Base
+
+class Email(Base):
+ __tablename__ = dbtableprefix + "email"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ short: Mapped[Optional[str]] = mapped_column(
+ String(250),
+ ForeignKey(dbtableprefix + "shortener.short", ondelete="CASCADE"),
+ )
+ all_addr: Mapped[Optional[str]] = mapped_column(Text)
+ to_addr: Mapped[Optional[str]] = mapped_column(Text)
+ cc_addr: Mapped[Optional[str]] = mapped_column(Text)
+ from_addr: Mapped[Optional[str]] = mapped_column(Text)
+ reply_to_addr: Mapped[Optional[str]] = mapped_column(Text)
+ return_path_addr: Mapped[Optional[str]] = mapped_column(Text)
+ subject: Mapped[Optional[str]] = mapped_column(Text)
+ datetime_message: Mapped[Optional[datetime]] = mapped_column(DateTime)
+ datetime_received: Mapped[Optional[datetime]] = mapped_column(DateTime)
+
+
+class EmailAttachment(Base):
+ __tablename__ = dbtableprefix + "emailattachment"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ email_id: Mapped[Optional[int]] = mapped_column(
+ Integer(),
+ ForeignKey(dbtableprefix + "email.id", ondelete="CASCADE"),
+ )
+ index: Mapped[Optional[int]] = mapped_column(Integer)
+ content_type: Mapped[Optional[str]] = mapped_column(Text)
+ extension: Mapped[Optional[str]] = mapped_column(Text)
+ upload: Mapped[Optional[int]] = mapped_column(
+ Integer(),
+ ForeignKey(dbtableprefix + "uploads.indexno", ondelete="CASCADE"),
+ )
+
+
+class Shortener(Base):
+ __tablename__ = dbtableprefix + "shortener"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ short: Mapped[str] = mapped_column(String(250), unique=True)
+ filename: Mapped[Optional[str]] = mapped_column(String(255), index=True)
+ uid: Mapped[Optional[str]] = mapped_column(String(250))
+ user_id: Mapped[Optional[int]] = mapped_column(
+ Integer(),
+ ForeignKey(dbtableprefix + "user.id", ondelete="CASCADE"),
+ )
+ temp_user_id: Mapped[Optional[int]] = mapped_column(
+ Integer(),
+ ForeignKey(dbtableprefix + "tempuser.id", ondelete="CASCADE"),
+ )
+ key: Mapped[Optional[str]] = mapped_column(String(255), index=True)
+ index: Mapped[Optional[int]] = mapped_column(Integer)
+ modtime: Mapped[Optional[datetime]] = mapped_column(DateTime, server_default=func.now()) # pylint: disable=not-callable
diff --git a/docassemble_webapp/docassemble/webapp/errors.py b/docassemble_webapp/docassemble/webapp/errors.py
new file mode 100644
index 000000000..28ac8486b
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/errors.py
@@ -0,0 +1,311 @@
+import json
+import traceback
+import re
+import os
+import tempfile
+from jinja2.exceptions import TemplateError
+from markupsafe import Markup
+from flask_wtf.csrf import CSRFError
+from bs4 import BeautifulSoup
+import werkzeug.exceptions
+from flask import (
+ request,
+ redirect,
+ jsonify,
+ render_template,
+ flash,
+ session,
+ current_app,
+)
+from flask_login import current_user
+from docassemble.base.error import (
+ DANotFoundError,
+ DAInvalidFilename,
+ DAError,
+ DASourceError,
+)
+from docassemble.base.functions import get_message_log, all_variables, interview_path
+from docassemble.base.language.control import get_language
+from docassemble.base.language.words import word
+from docassemble.base.save_status import SS_NEW, SS_IGNORE
+from docassemble.base.thread_context import this_thread
+from docassemble.base.util import markdown_to_html
+from docassemble.webapp.config import (
+ ERROR_TYPES_NO_EMAIL,
+ DEBUG,
+ NOTIFICATION_CONTAINER,
+ NOTIFICATION_MESSAGE,
+ DEFER,
+ DEFAULT_LANGUAGE,
+ LOGFILE,
+ daconfig,
+)
+from docassemble.webapp.daredis import r
+from docassemble.webapp.interview.helpers import get_history, get_part
+from docassemble.webapp.interview.views import index
+from docassemble.webapp.lock import release_lock
+from docassemble.webapp.mail.da_flask_mail import Message
+from docassemble.webapp.mail.hooks import da_send_mail
+from docassemble.webapp.translations import setup_translation
+from docassemble.webapp.utils.helpers import noquote, get_requester_ip
+from docassemble.webapp.utils.hooks import url_for
+from docassemble.webapp.utils.logger import logmessage
+
+def init_app(app):
+ @app.errorhandler(404)
+ def page_not_found_error(the_error): # pylint: disable=unused-argument
+ return render_template('pages/404.html'), 404
+
+
+ @app.errorhandler(Exception)
+ def server_error(the_error):
+ setup_translation()
+ if hasattr(the_error, 'interview') and the_error.interview.debug and hasattr(the_error, 'interview_status'):
+ the_history = get_history(the_error.interview, the_error.interview_status)
+ else:
+ the_history = None
+ the_vars = None
+ if isinstance(the_error, DASourceError):
+ if (DEBUG and daconfig.get('development site is protected', False)) or (current_user.is_authenticated and current_user.has_role('admin', 'developer')):
+ errmess = str(the_error)
+ else:
+ errmess = word("There was an error. Please contact the system administrator.")
+ the_trace = None
+ logmessage(str(the_error))
+ elif isinstance(the_error, (DAError, DANotFoundError, DAInvalidFilename)):
+ errmess = str(the_error)
+ the_trace = None
+ logmessage(errmess)
+ elif isinstance(the_error, TemplateError):
+ errmess = str(the_error)
+ if hasattr(the_error, 'name') and the_error.name is not None:
+ errmess += "\nName: " + str(the_error.name)
+ if hasattr(the_error, 'filename') and the_error.filename is not None:
+ errmess += "\nFilename: " + str(the_error.filename)
+ if hasattr(the_error, 'docx_context'):
+ errmess += "\n\nContext:\n" + "\n".join(map(lambda x: " " + x, the_error.docx_context))
+ the_trace = traceback.format_exc()
+ try:
+ logmessage(errmess)
+ except:
+ logmessage("Could not log the error message")
+ else:
+ try:
+ errmess = str(type(the_error).__name__) + ": " + str(the_error)
+ except:
+ errmess = str(type(the_error).__name__)
+ if hasattr(the_error, 'traceback'):
+ the_trace = the_error.traceback
+ else:
+ the_trace = traceback.format_exc()
+ if hasattr(this_thread, 'misc') and 'current_field' in this_thread.misc:
+ errmess += "\nIn field index number " + str(this_thread.misc['current_field'])
+ if hasattr(the_error, 'da_line_with_error'):
+ errmess += "\nIn line: " + str(the_error.da_line_with_error)
+ try:
+ logmessage(errmess)
+ except:
+ logmessage("Could not log the error message")
+ logmessage(the_trace)
+ if isinstance(the_error, DAError):
+ error_code = the_error.error_code
+ if isinstance(the_error, DANotFoundError):
+ error_code = 404
+ elif isinstance(the_error, werkzeug.exceptions.HTTPException):
+ error_code = the_error.code
+ else:
+ error_code = 501
+ if hasattr(the_error, 'user_dict'):
+ the_vars = the_error.user_dict
+ if hasattr(the_error, 'interview'):
+ special_error_markdown = the_error.interview.consolidated_metadata.get('error help', None)
+ if isinstance(special_error_markdown, dict):
+ language = get_language()
+ if language in special_error_markdown:
+ special_error_markdown = special_error_markdown[language]
+ elif '*' in special_error_markdown:
+ special_error_markdown = special_error_markdown['*']
+ elif DEFAULT_LANGUAGE in special_error_markdown:
+ special_error_markdown = special_error_markdown[DEFAULT_LANGUAGE]
+ else:
+ special_error_markdown = None
+ else:
+ special_error_markdown = None
+ if special_error_markdown is None:
+ special_error_markdown = daconfig.get('error help', None)
+ if special_error_markdown is not None:
+ special_error_html = markdown_to_html(special_error_markdown)
+ else:
+ special_error_html = None
+ flask_logtext = []
+ if os.path.exists(LOGFILE):
+ with open(LOGFILE, encoding='utf-8') as the_file:
+ for line in the_file:
+ if re.match('Exception', line):
+ flask_logtext = []
+ flask_logtext.append(line)
+ orig_errmess = errmess
+ errmess = noquote(errmess)
+ if re.search(r'\n', errmess):
+ errmess = '' + errmess + '
'
+ else:
+ errmess = '' + errmess + '
'
+ initial_values = {
+ "daMessageLog": get_message_log(),
+ "daNotificationContainer": NOTIFICATION_CONTAINER % ('',),
+ "daNotificationMessage": NOTIFICATION_MESSAGE,
+ }
+ script = f"""
+
+ """
+ error_notification(the_error, message=errmess, history=the_history, trace=the_trace, the_request=request, the_vars=the_vars)
+ if (request.path.endswith('/interview') or request.path.endswith('/start') or request.path.endswith('/run')) and interview_path() is not None:
+ if this_thread.misc.get('save_status', SS_NEW) != SS_IGNORE:
+ try:
+ release_lock(this_thread.current_info['session'], this_thread.current_info['yaml_filename'])
+ except:
+ pass
+ if 'in error' not in session and this_thread.interview is not None and 'error action' in this_thread.interview.consolidated_metadata:
+ session['in error'] = True
+ return index(action_argument={'action': this_thread.interview.consolidated_metadata['error action'], 'arguments': {'error_message': orig_errmess, 'error_history': the_history, 'error_trace': the_trace}}, refer=['error'])
+ if int(int(error_code)/100) == 4:
+ show_debug = False
+ elif isinstance(the_error, (DAError, DAInvalidFilename)):
+ show_debug = False
+ elif DEBUG and daconfig.get('development site is protected', False):
+ show_debug = True
+ elif current_user.is_authenticated and current_user.has_role('admin', 'developer'):
+ show_debug = True
+ else:
+ show_debug = False
+ if error_code == 404:
+ the_template = 'pages/404.html'
+ else:
+ the_template = 'pages/501.html'
+ try:
+ yaml_filename = interview_path()
+ except:
+ yaml_filename = None
+ show_retry = request.path.endswith('/interview') or request.path.endswith('/start') or request.path.endswith('/run')
+ extra_js = Markup(script)
+ error_page_extra_js = get_part('error page extra javascript')
+ if isinstance(error_page_extra_js, Markup):
+ extra_js += error_page_extra_js
+ return render_template(the_template, verbose=daconfig.get('verbose error messages', True), version_warning=None, error=errmess, historytext=str(the_history), logtext=str(the_trace), extra_js=extra_js, special_error=special_error_html, show_debug=show_debug, yaml_filename=yaml_filename, show_retry=show_retry), error_code
+
+
+ @app.errorhandler(CSRFError)
+ def handle_csrf_error(the_error):
+ if request.method == 'POST' and '/checkout' not in request.url:
+ setup_translation()
+ if 'ajax' in request.form and int(request.form['ajax']):
+ flash(word("Input not processed because the page expired."), "success")
+ return jsonify({'action': 'reload', 'reason': 'csrf_error'})
+ try:
+ referer = str(request.referrer)
+ except:
+ referer = None
+ if referer and referer != 'None':
+ flash(word("Input not processed because the page expired."), "success")
+ return redirect(referer)
+ return server_error(the_error)
+
+
+def error_notification(err, message=None, history=None, trace=None, referer=None, the_request=None, the_vars=None):
+ recipient_email = daconfig.get('error notification email', None)
+ if not recipient_email:
+ return
+ if err.__class__.__name__ in ['CSRFError', 'ClientDisconnected', 'MethodNotAllowed', 'DANotFoundError', 'DAInvalidFilename'] + ERROR_TYPES_NO_EMAIL:
+ return
+ email_recipients = []
+ if isinstance(recipient_email, list):
+ email_recipients.extend(recipient_email)
+ else:
+ email_recipients.append(recipient_email)
+ if message is None:
+ errmess = str(err)
+ else:
+ errmess = message
+ try:
+ email_address = current_user.email
+ except:
+ email_address = None
+ if the_request:
+ try:
+ referer = str(the_request.referrer)
+ except:
+ referer = None
+ ipaddress = get_requester_ip(the_request)
+ else:
+ referer = None
+ ipaddress = None
+ if daconfig.get('error notification variables', DEBUG):
+ if the_vars is None:
+ try:
+ the_vars = all_variables(include_internal=True)
+ except:
+ pass
+ else:
+ the_vars = None
+ json_filename = None
+ if the_vars is not None and len(the_vars):
+ try:
+ with tempfile.NamedTemporaryFile(mode='w', prefix="datemp", suffix='.json', delete=False, encoding='utf-8') as fp:
+ fp.write(json.dumps(the_vars, sort_keys=True, indent=2))
+ json_filename = fp.name
+ except:
+ pass
+ the_interview_path = interview_path()
+ try:
+ the_key = 'da:errornotification:' + str(ipaddress)
+ existing = r.get(the_key)
+ pipe = r.pipeline()
+ pipe.set(the_key, 1)
+ pipe.expire(the_key, 60)
+ pipe.execute()
+ if existing:
+ return
+ except:
+ pass
+ try:
+ try:
+ html = "\n \n There was an error in the " + current_app.config['APP_NAME'] + " current_application.
\n The error message was:
\n" + err.__class__.__name__ + ": " + str(errmess) + "
\n"
+ body = "There was an error in the " + current_app.config['APP_NAME'] + " application.\n\nThe error message was:\n\n" + err.__class__.__name__ + ": " + str(errmess)
+ if trace is not None:
+ body += "\n\n" + str(trace)
+ html += "" + str(trace) + "
"
+ if history is not None:
+ body += "\n\n" + BeautifulSoup(history, "html.parser").get_text('\n')
+ html += history
+ if referer is not None and referer != 'None':
+ body += "\n\nThe referer URL was " + str(referer)
+ html += "The referer URL was " + str(referer) + "
"
+ elif the_interview_path is not None:
+ body += "\n\nThe interview was " + str(the_interview_path)
+ html += "The interview was " + str(the_interview_path) + "
"
+ if email_address is not None:
+ body += "\n\nThe user was " + str(email_address)
+ html += "The user was " + str(email_address) + "
"
+ if trace is not None:
+ body += "\n\n" + str(trace)
+ html += "" + str(trace) + "
"
+ if 'external hostname' in daconfig and daconfig['external hostname'] is not None:
+ body += "\n\nThe external hostname was " + str(daconfig['external hostname'])
+ html += "The external hostname was " + str(daconfig['external hostname']) + "
"
+ html += "\n \n"
+ msg = Message(current_app.config['APP_NAME'] + " error: " + err.__class__.__name__, recipients=email_recipients, body=body, html=html)
+ if json_filename:
+ with open(json_filename, 'r', encoding='utf-8') as fp:
+ msg.attach('variables.json', 'application/json', fp.read())
+ da_send_mail(msg, None)
+ except BaseException as zerr:
+ logmessage(str(zerr))
+ body = "There was an error in the " + current_app.config['APP_NAME'] + " application."
+ html = "\n \n There was an error in the " + current_app.config['APP_NAME'] + " application.
\n \n"
+ msg = Message(current_app.config['APP_NAME'] + " error: " + err.__class__.__name__, recipients=email_recipients, body=body, html=html)
+ if json_filename:
+ with open(json_filename, 'r', encoding='utf-8') as fp:
+ msg.attach('variables.json', 'application/json', fp.read())
+ da_send_mail(msg, None)
+ except:
+ pass
diff --git a/docassemble_webapp/docassemble/webapp/extensions.py b/docassemble_webapp/docassemble/webapp/extensions.py
new file mode 100644
index 000000000..cad47785d
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/extensions.py
@@ -0,0 +1,16 @@
+from flask_wtf.csrf import CSRFProtect
+from flask_babel import Babel
+from flask_cors import CORS
+from flask_login import LoginManager
+from flask_sqlalchemy import SQLAlchemy
+from docassemble_flask_user import UserManager
+from docassemblekvsession import KVSessionExtension
+from docassemble.webapp.db_base import Base
+
+csrf = CSRFProtect()
+babel = Babel()
+cors = CORS()
+lm = LoginManager()
+the_user_manager = UserManager()
+kv_session = KVSessionExtension()
+db = SQLAlchemy(model_class=Base)
diff --git a/docassemble_webapp/docassemble/webapp/fax/__init__.py b/docassemble_webapp/docassemble/webapp/fax/__init__.py
new file mode 100644
index 000000000..f34cff3f2
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/fax/__init__.py
@@ -0,0 +1 @@
+from .views import fax_bp # noqa: F401
diff --git a/docassemble_webapp/docassemble/webapp/clicksend.py b/docassemble_webapp/docassemble/webapp/fax/clicksend.py
similarity index 87%
rename from docassemble_webapp/docassemble/webapp/clicksend.py
rename to docassemble_webapp/docassemble/webapp/fax/clicksend.py
index 71530919f..a3ec73fb7 100644
--- a/docassemble_webapp/docassemble/webapp/clicksend.py
+++ b/docassemble_webapp/docassemble/webapp/fax/clicksend.py
@@ -19,14 +19,14 @@ def send_fax(fax_number, the_file, config, country=None):
else:
upload_file = clicksend_client.UploadFile(content=codecs.encode(the_file.slurp(auto_decode=False), 'base64'))
try:
- api_response = api_instance.uploads_post(upload_file, 'fax')
+ api_response = api_instance.uploads_post(upload_file, 'fax') # pylint: disable=no-member
except ApiException as e:
- raise DAException("Exception when calling UploadApi->uploads_post: %s\n" % e)
+ raise DAException("Exception when calling UploadApi->uploads_post: %s\n" % e) from e
try:
response = ast.literal_eval(api_response)
- except:
+ except Exception as e:
logmessage(api_response)
- raise DAException("Exception when calling UploadApi->uploads_post: response could not be parsed: " + api_response)
+ raise DAException("Exception when calling UploadApi->uploads_post: response could not be parsed: " + api_response) from e
if response.get('http_code', 0) != 200:
logmessage(api_response)
raise DAException("Exception when calling UploadApi->uploads_post: response code not 200: " + api_response)
@@ -38,17 +38,17 @@ def send_fax(fax_number, the_file, config, country=None):
try:
api_response = api_instance.fax_send_post(fax_message)
except ApiException as e:
- raise DAException("Exception when calling FAXApi->fax_send_post: %s\n" % e)
+ raise DAException("Exception when calling FAXApi->fax_send_post: %s\n" % e) from e
try:
response = ast.literal_eval(api_response)
- except:
+ except Exception as e:
logmessage(api_response)
- raise DAException("Exception when calling FAXApi->fax_send_post: response could not be parsed: " + api_response)
+ raise DAException("Exception when calling FAXApi->fax_send_post: response could not be parsed: " + api_response) from e
if response.get('http_code', 0) != 200:
logmessage(api_response)
raise DAException("Exception when calling FAXApi->fax_send_post: response code not 200: " + api_response)
try:
response['data']['messages'][0]['message_id']
- except:
- raise DAException("Exception when calling FAXApi->fax_send_post: message_id not in response" + api_response)
+ except Exception as e:
+ raise DAException("Exception when calling FAXApi->fax_send_post: message_id not in response" + api_response) from e
return response['data']['messages'][0]
diff --git a/docassemble_webapp/docassemble/webapp/fax/helpers.py b/docassemble_webapp/docassemble/webapp/fax/helpers.py
new file mode 100644
index 000000000..0975b5959
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/fax/helpers.py
@@ -0,0 +1,128 @@
+import json
+from docassemble.base.error import DAException
+from docassemble.webapp.app_object import flaskapp as app
+from docassemble.webapp.config import daconfig, DEFAULT_COUNTRY
+from docassemble.webapp.daredis import r
+from docassemble.webapp.hooks.impl import hookimpl
+from docassemble.webapp.twilio.helpers import twilio_config
+from docassemble.webapp.utils.hooks import url_for
+from docassemble.webapp.utils.logger import logmessage
+
+fax_provider = daconfig.get('fax provider', None) or 'clicksend'
+
+clicksend_config = {}
+telnyx_config = {}
+
+
+def get_clicksend_config():
+ if 'clicksend' in daconfig and isinstance(daconfig['clicksend'], (list, dict)):
+ the_clicksend_config = {'name': {}, 'number': {}}
+ if isinstance(daconfig['clicksend'], dict):
+ config_list = [daconfig['clicksend']]
+ else:
+ config_list = daconfig['clicksend']
+ for the_config in config_list:
+ if isinstance(the_config, dict) and 'api username' in the_config and 'api key' in the_config and 'number' in the_config:
+ if 'country' not in the_config:
+ the_config['country'] = DEFAULT_COUNTRY or 'US'
+ if 'from email' not in the_config:
+ the_config['from email'] = app.config['MAIL_DEFAULT_SENDER']
+ the_clicksend_config['number'][str(the_config['number'])] = the_config
+ if 'default' not in the_clicksend_config['name']:
+ the_clicksend_config['name']['default'] = the_config
+ if 'name' in the_config:
+ the_clicksend_config['name'][the_config['name']] = the_config
+ else:
+ logmessage("improper setup in clicksend configuration")
+ if 'default' not in the_clicksend_config['name']:
+ the_clicksend_config = None
+ else:
+ the_clicksend_config = None
+ # if fax_provider == 'clicksend' and the_clicksend_config is None:
+ # logmessage("improper clicksend configuration; faxing will not be functional")
+ return the_clicksend_config
+
+def get_telnyx_config():
+ if 'telnyx' in daconfig and isinstance(daconfig['telnyx'], (list, dict)):
+ the_telnyx_config = {'name': {}, 'number': {}}
+ if isinstance(daconfig['telnyx'], dict):
+ config_list = [daconfig['telnyx']]
+ else:
+ config_list = daconfig['telnyx']
+ for the_config in config_list:
+ if isinstance(the_config, dict) and 'app id' in the_config and 'api key' in the_config and 'number' in the_config:
+ if 'country' not in the_config:
+ the_config['country'] = DEFAULT_COUNTRY or 'US'
+ if 'from email' not in the_config:
+ the_config['from email'] = app.config['MAIL_DEFAULT_SENDER']
+ the_telnyx_config['number'][str(the_config['number'])] = the_config
+ if 'default' not in the_telnyx_config['name']:
+ the_telnyx_config['name']['default'] = the_config
+ if 'name' in the_config:
+ the_telnyx_config['name'][the_config['name']] = the_config
+ else:
+ logmessage("improper setup in twilio configuration")
+ if 'default' not in the_telnyx_config['name']:
+ the_telnyx_config = None
+ else:
+ the_telnyx_config = None
+ if fax_provider == 'telnyx' and the_telnyx_config is None:
+ logmessage("improper telnyx configuration; faxing will not be functional")
+ return the_telnyx_config
+
+@hookimpl(specname="send_fax")
+def da_send_fax(fax_number, the_file, config, country):
+ if clicksend_config is not None and fax_provider == 'clicksend':
+ if config not in clicksend_config['name']:
+ raise DAException("There is no ClickSend configuration called " + str(config))
+ import docassemble.webapp.fax.clicksend
+ info = docassemble.webapp.fax.clicksend.send_fax(fax_number, the_file, clicksend_config['name'][config], country)
+ the_key = 'da:faxcallback:sid:' + info['message_id']
+ pipe = r.pipeline()
+ pipe.set(the_key, json.dumps(info))
+ pipe.expire(the_key, 86400)
+ pipe.execute()
+ return info['message_id']
+ if telnyx_config is not None and fax_provider == 'telnyx':
+ if config not in telnyx_config['name']:
+ raise DAException("There is no Telnyx configuration called " + str(config))
+ import docassemble.webapp.fax.telnyx
+ info = docassemble.webapp.fax.telnyx.send_fax(fax_number, the_file, telnyx_config['name'][config], country)
+ the_key = 'da:faxcallback:sid:' + info['id']
+ pipe = r.pipeline()
+ pipe.set(the_key, json.dumps(info))
+ pipe.expire(the_key, 86400)
+ pipe.execute()
+ return info['id']
+ if twilio_config is None:
+ logmessage("da_send_fax: ignoring call to da_send_fax because Twilio not enabled")
+ return None
+ if config not in twilio_config['name'] or 'fax' not in twilio_config['name'][config] or twilio_config['name'][config]['fax'] in (False, None):
+ logmessage("da_send_fax: ignoring call to da_send_fax because fax feature not enabled")
+ return None
+ account_sid = twilio_config['name'][config].get('account sid', None)
+ auth_token = twilio_config['name'][config].get('auth token', None)
+ from_number = twilio_config['name'][config].get('number', None)
+ if account_sid is None or auth_token is None or from_number is None:
+ logmessage("da_send_fax: ignoring call to da_send_fax because account sid, auth token, and/or number missing")
+ return None
+ from twilio.rest import Client as TwilioRestClient
+ client = TwilioRestClient(account_sid, auth_token)
+ fax = client.fax.v1.faxes.create( # pylint: disable=no-member
+ from_=from_number,
+ to=fax_number,
+ media_url=the_file.url_for(temporary=True, seconds=600),
+ status_callback=url_for('fax.fax_callback', _external=True)
+ )
+ return fax.sid
+
+
+def populate_fax_config():
+ clicksend_config.clear()
+ conf = get_clicksend_config()
+ if conf is not None:
+ clicksend_config.update(conf)
+ telnyx_config.clear()
+ conf = get_telnyx_config()
+ if conf is not None:
+ telnyx_config.update()
diff --git a/docassemble_webapp/docassemble/webapp/telnyx.py b/docassemble_webapp/docassemble/webapp/fax/telnyx.py
similarity index 80%
rename from docassemble_webapp/docassemble/webapp/telnyx.py
rename to docassemble_webapp/docassemble/webapp/fax/telnyx.py
index f3a3dd5dc..2316c3807 100644
--- a/docassemble_webapp/docassemble/webapp/telnyx.py
+++ b/docassemble_webapp/docassemble/webapp/fax/telnyx.py
@@ -1,11 +1,10 @@
import requests
-import docassemble.base.util
from docassemble.base.error import DAException
-
+from docassemble.base.util import pdf_concatenate
def send_fax(fax_number, the_file, config, country=None): # pylint: disable=unused-argument
if not bool((hasattr(the_file, 'extension') and the_file.extension == 'pdf') or (hasattr(the_file, 'mimetype') and the_file.mimetype == 'application/pdf')):
- the_file = docassemble.base.util.pdf_concatenate(the_file)
+ the_file = pdf_concatenate(the_file)
telnyx_api_endpoint = config.get('api endpoint', 'https://api.telnyx.com/v2/faxes')
headers = {"Authorization": "Bearer " + config['api key']}
media_url = the_file.url_for(temporary=True, seconds=600)
@@ -14,14 +13,14 @@ def send_fax(fax_number, the_file, config, country=None): # pylint: disable=unu
raise DAException("Exception when calling Telnyx: status code is " + str(r.status_code) + " and response is " + r.text)
try:
response = r.json()
- except:
- raise DAException("Exception when calling Telnyx: response not JSON: " + r.text)
+ except Exception as err:
+ raise DAException("Exception when calling Telnyx: response not JSON: " + r.text) from err
try:
response['data']['id']
- except:
- raise DAException("Exception when calling Telnyx: id not in response: " + r.text)
+ except Exception as err:
+ raise DAException("Exception when calling Telnyx: id not in response: " + r.text) from err
try:
response['data']['latest_update_time'] = response['data']['created_at']
- except:
- raise DAException("Exception when calling Telnyx: could not find created_at time " + r.text)
+ except Exception as err:
+ raise DAException("Exception when calling Telnyx: could not find created_at time " + r.text) from err
return response['data']
diff --git a/docassemble_webapp/docassemble/webapp/fax/views.py b/docassemble_webapp/docassemble/webapp/fax/views.py
new file mode 100644
index 000000000..95103f2e3
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/fax/views.py
@@ -0,0 +1,109 @@
+import json
+from flask import request, Blueprint
+from docassemble.webapp.daredis import r
+from docassemble.webapp.extensions import csrf
+from docassemble.webapp.twilio.helpers import twilio_config
+from docassemble.webapp.utils.logger import logmessage
+from .helpers import telnyx_config, clicksend_config, fax_provider
+
+fax_bp = Blueprint(
+ 'fax',
+ __name__
+)
+
+@fax_bp.route("/fax_callback", methods=['POST'])
+@csrf.exempt
+def fax_callback():
+ if twilio_config is None:
+ logmessage("fax_callback: Twilio not enabled")
+ return ('', 204)
+ post_data = request.form.copy()
+ if 'FaxSid' not in post_data or 'AccountSid' not in post_data:
+ logmessage("fax_callback: FaxSid and/or AccountSid missing")
+ return ('', 204)
+ tconfig = None
+ for config_name, config_info in twilio_config['name'].items(): # pylint: disable=unused-variable
+ if 'account sid' in config_info and config_info['account sid'] == post_data['AccountSid']:
+ tconfig = config_info
+ if tconfig is None:
+ logmessage("fax_callback: account sid of fax callback did not match any account sid in the Twilio configuration")
+ if 'fax' not in tconfig or tconfig['fax'] in (False, None):
+ logmessage("fax_callback: fax feature not enabled")
+ return ('', 204)
+ params = {}
+ for param in ('FaxSid', 'From', 'To', 'RemoteStationId', 'FaxStatus', 'ApiVersion', 'OriginalMediaUrl', 'NumPages', 'MediaUrl', 'ErrorCode', 'ErrorMessage'):
+ params[param] = post_data.get(param, None)
+ the_key = 'da:faxcallback:sid:' + post_data['FaxSid']
+ pipe = r.pipeline()
+ pipe.set(the_key, json.dumps(params))
+ pipe.expire(the_key, 86400)
+ pipe.execute()
+ return ('', 204)
+
+
+@fax_bp.route("/clicksend_fax_callback", methods=['POST'])
+@csrf.exempt
+def clicksend_fax_callback():
+ if clicksend_config is None or fax_provider != 'clicksend':
+ logmessage("clicksend_fax_callback: Clicksend not enabled")
+ return ('', 204)
+ post_data = request.form.copy()
+ if 'message_id' not in post_data:
+ logmessage("clicksend_fax_callback: message_id missing")
+ return ('', 204)
+ the_key = 'da:faxcallback:sid:' + post_data['message_id']
+ the_json = r.get(the_key)
+ try:
+ params = json.loads(the_json)
+ except:
+ logmessage("clicksend_fax_callback: existing fax record could not be found")
+ return ('', 204)
+ for param in ('timestamp_send', 'timestamp', 'message_id', 'status', 'status_code', 'status_text', 'error_code', 'error_text', 'custom_string', 'user_id', 'subaccount_id', 'message_type'):
+ params[param] = post_data.get(param, None)
+ pipe = r.pipeline()
+ pipe.set(the_key, json.dumps(params))
+ pipe.expire(the_key, 86400)
+ pipe.execute()
+ return ('', 204)
+
+
+@fax_bp.route("/telnyx_fax_callback", methods=['POST'])
+@csrf.exempt
+def telnyx_fax_callback():
+ if telnyx_config is None:
+ logmessage("telnyx_fax_callback: Telnyx not enabled")
+ return ('', 204)
+ data = request.get_json(silent=True)
+ try:
+ the_id = data['data']['payload']['fax_id']
+ except:
+ logmessage("telnyx_fax_callback: fax_id not found")
+ return ('', 204)
+ the_key = 'da:faxcallback:sid:' + str(the_id)
+ the_json = r.get(the_key)
+ try:
+ params = json.loads(the_json)
+ except:
+ logmessage("telnyx_fax_callback: existing fax record could not be found")
+ return ('', 204)
+ try:
+ params['status'] = data['data']['payload']['status']
+ if params['status'] == 'failed' and 'failure_reason' in data['data']['payload']:
+ params['status'] += ': ' + data['data']['payload']['failure_reason']
+ logmessage("telnyx_fax_callback: failure because " + data['data']['payload']['failure_reason'])
+ except:
+ logmessage("telnyx_fax_callback: could not find status")
+ try:
+ params['latest_update_time'] = data['data']['occurred_at']
+ except:
+ logmessage("telnyx_fax_callback: could not update latest_update_time")
+ if 'status' in params and params['status'] == 'delivered':
+ try:
+ params['page_count'] = data['data']['payload']['page_count']
+ except:
+ logmessage("telnyx_fax_callback: could not update page_count")
+ pipe = r.pipeline()
+ pipe.set(the_key, json.dumps(params))
+ pipe.expire(the_key, 86400)
+ pipe.execute()
+ return ('', 204)
diff --git a/docassemble_webapp/docassemble/webapp/file_number.py b/docassemble_webapp/docassemble/webapp/file_number.py
deleted file mode 100644
index 8246bb5bb..000000000
--- a/docassemble_webapp/docassemble/webapp/file_number.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from docassemble.webapp.db_object import db
-from docassemble.webapp.core.models import Uploads
-
-
-def get_new_file_number(user_code, file_name, yaml_file_name=None):
- new_upload = Uploads(key=user_code, filename=file_name, yamlfile=yaml_file_name)
- db.session.add(new_upload)
- db.session.commit()
- return new_upload.indexno
diff --git a/docassemble_webapp/docassemble/webapp/files/__init__.py b/docassemble_webapp/docassemble/webapp/files/__init__.py
new file mode 100644
index 000000000..bbf73d9d6
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/files/__init__.py
@@ -0,0 +1 @@
+from .blueprint import files_bp # noqa: F401
diff --git a/docassemble_webapp/docassemble/webapp/files/api.py b/docassemble_webapp/docassemble/webapp/files/api.py
new file mode 100644
index 000000000..e4c1816e5
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/files/api.py
@@ -0,0 +1,53 @@
+import os
+import re
+import werkzeug
+from flask import request
+from flask_cors import cross_origin
+from flask_login import current_user
+from docassemble.webapp.api.helpers import api_verify
+from docassemble.webapp.utils.helpers import jsonify_with_status, custom_send_file
+from docassemble.webapp.sessions import get_session_uids
+from docassemble.webapp.utils.filenames import (
+ get_ext_and_mimetype,
+ secure_filename_unicode_ok,
+)
+from .file_access import get_info_from_file_number
+from .blueprint import files_bp
+
+@files_bp.route('/api/file/', methods=['GET'])
+@cross_origin(origins='*', methods=['GET', 'HEAD'], automatic_options=True)
+def api_file(file_number):
+ if not api_verify():
+ return jsonify_with_status("Access denied.", 403)
+ # yaml_filename = request.args.get('i', None)
+ # session_id = request.args.get('session', None)
+ number = re.sub(r'[^0-9]', '', str(file_number))
+ privileged = bool(current_user.is_authenticated and current_user.has_role('admin', 'advocate'))
+ try:
+ file_info = get_info_from_file_number(number, privileged=privileged, uids=get_session_uids())
+ except:
+ return ('File not found', 404)
+ if 'path' not in file_info:
+ return ('File not found', 404)
+ if 'extension' in request.args:
+ extension = werkzeug.utils.secure_filename(request.args['extension'])
+ if os.path.isfile(file_info['path'] + '.' + extension):
+ the_path = file_info['path'] + '.' + extension
+ extension, mimetype = get_ext_and_mimetype(file_info['path'] + '.' + extension)
+ else:
+ return ('File not found', 404)
+ elif 'filename' in request.args:
+ the_filename = secure_filename_unicode_ok(request.args['filename'])
+ if os.path.isfile(os.path.join(os.path.dirname(file_info['path']), the_filename)):
+ the_path = os.path.join(os.path.dirname(file_info['path']), the_filename)
+ extension, mimetype = get_ext_and_mimetype(the_filename)
+ else:
+ return ('File not found', 404)
+ else:
+ the_path = file_info['path']
+ mimetype = file_info['mimetype']
+ if not os.path.isfile(the_path):
+ return ('File not found', 404)
+ response = custom_send_file(the_path, mimetype=mimetype)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
diff --git a/docassemble_webapp/docassemble/webapp/files/blueprint.py b/docassemble_webapp/docassemble/webapp/files/blueprint.py
new file mode 100644
index 000000000..57bc1da54
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/files/blueprint.py
@@ -0,0 +1,6 @@
+from flask import Blueprint
+
+files_bp = Blueprint(
+ 'files',
+ __name__
+)
diff --git a/docassemble_webapp/docassemble/webapp/files/common.py b/docassemble_webapp/docassemble/webapp/files/common.py
new file mode 100644
index 000000000..5a4a7864c
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/files/common.py
@@ -0,0 +1,34 @@
+from flask import session
+from flask_login import current_user
+from sqlalchemy import and_, select
+from docassemble.base.functions import get_uid
+from docassemble.webapp.extensions import db
+from docassemble.webapp.interview.models import UserDictKeys
+from docassemble.webapp.main.models import UploadsRoleAuth, Uploads, UploadsUserAuth
+from docassemble.webapp.users.models import UserRoles
+
+# @elapsed('can_access_file_number')
+def can_access_file_number(file_number, uids=None):
+ upload = db.session.execute(select(Uploads).where(Uploads.indexno == file_number)).scalar()
+ if upload is None:
+ return False
+ if current_user and current_user.is_authenticated and current_user.has_role('admin', 'developer', 'advocate', 'trainer'):
+ return True
+ if not upload.private:
+ return True
+ if uids is None or len(uids) == 0:
+ new_uid = get_uid()
+ if new_uid is not None:
+ uids = [new_uid]
+ else:
+ uids = []
+ if upload.key in uids:
+ return True
+ if current_user and current_user.is_authenticated:
+ if db.session.execute(select(UserDictKeys).filter_by(key=upload.key, user_id=current_user.id)).first() or db.session.execute(select(UploadsUserAuth).filter_by(uploads_indexno=file_number, user_id=current_user.id)).first() or db.session.execute(select(UploadsRoleAuth).join(UserRoles, and_(UserRoles.user_id == current_user.id, UploadsRoleAuth.role_id == UserRoles.role_id)).where(UploadsRoleAuth.uploads_indexno == file_number)).first():
+ return True
+ elif session and 'tempuser' in session:
+ temp_user_id = int(session['tempuser'])
+ if db.session.execute(select(UserDictKeys).filter_by(key=upload.key, temp_user_id=temp_user_id)).first() or db.session.execute(select(UploadsUserAuth).filter_by(uploads_indexno=file_number, temp_user_id=temp_user_id)).first():
+ return True
+ return False
diff --git a/docassemble_webapp/docassemble/webapp/file_access.py b/docassemble_webapp/docassemble/webapp/files/file_access.py
similarity index 91%
rename from docassemble_webapp/docassemble/webapp/file_access.py
rename to docassemble_webapp/docassemble/webapp/files/file_access.py
index c348594c2..7c6762e89 100644
--- a/docassemble_webapp/docassemble/webapp/file_access.py
+++ b/docassemble_webapp/docassemble/webapp/files/file_access.py
@@ -6,23 +6,27 @@
# import sys
import tempfile
import xml.etree.ElementTree as ET
+from flask import session, url_for
+from flask_login import current_user
+from sqlalchemy import and_, select
from pikepdf import Pdf
from PIL import Image
+from docassemble.base.error import DAException
+from docassemble.base.functions import (
+ get_uid,
+ get_current_package,
+ static_filename_path,
+)
from docassemble.base.generate_key import random_lower_string
from docassemble.base.logger import logmessage
-from docassemble.base.error import DAException
-import docassemble.base.config
-import docassemble.base.functions
-from docassemble.webapp.core.models import Uploads, UploadsUserAuth, UploadsRoleAuth
-from docassemble.webapp.db_object import db
-from docassemble.webapp.files import SavedFile, get_ext_and_mimetype
-from docassemble.webapp.users.models import UserDictKeys, UserRoles
-import docassemble.webapp.cloud
-from flask import session, url_for
-from flask_login import current_user
-from sqlalchemy import and_, select
-
-cloud = docassemble.webapp.cloud.get_cloud()
+from docassemble.webapp.cloud.utils import cloud
+from docassemble.webapp.config import daconfig
+from docassemble.webapp.extensions import db
+from docassemble.webapp.interview.models import UserDictKeys
+from docassemble.webapp.main.models import UploadsRoleAuth, Uploads, UploadsUserAuth
+from docassemble.webapp.users.models import UserRoles
+from docassemble.webapp.utils.filenames import get_ext_and_mimetype
+from .savedfile import SavedFile
QPDF_PATH = 'qpdf'
@@ -30,9 +34,9 @@
def url_if_exists(file_reference, **kwargs):
attach_parameter = '&attachment=1' if kwargs.get('_attachment', False) else ''
parts = file_reference.split(":")
- base_url = url_for('rootindex', _external=kwargs.get('_external', False)).rstrip('/')
+ base_url = url_for('interview.rootindex', _external=kwargs.get('_external', False)).rstrip('/')
if len(parts) == 2:
- if cloud and docassemble.base.config.daconfig.get('use cloud urls', False):
+ if cloud and daconfig.get('use cloud urls', False):
m = re.search(r'^docassemble\.playground([0-9]+)(.*)$', parts[0])
if m:
user_id = m.group(1)
@@ -55,7 +59,7 @@ def url_if_exists(file_reference, **kwargs):
filename = re.sub(r'^data/static/', '', parts[1])
version_parameter = get_version_parameter(parts[0])
return base_url + '/packagestatic/' + parts[0] + '/' + re.sub(r'^data/static/', '', parts[1]) + version_parameter + attach_parameter
- the_path = docassemble.base.functions.static_filename_path(file_reference)
+ the_path = static_filename_path(file_reference)
if the_path is None or not os.path.isfile(the_path):
return None
version_parameter = get_version_parameter(parts[0])
@@ -81,7 +85,7 @@ def get_info_from_file_reference(file_reference, **kwargs):
if re.search(r'^[0-9]+$', str(file_reference)):
uids = kwargs.get('uids', None)
if uids is None or len(uids) == 0:
- new_uid = docassemble.base.functions.get_uid()
+ new_uid = get_uid()
if new_uid is not None:
uids = [new_uid]
else:
@@ -107,7 +111,7 @@ def get_info_from_file_reference(file_reference, **kwargs):
possible_mimetype = 'text/plain'
result = {}
temp_file = tempfile.NamedTemporaryFile(prefix="datemp", suffix='.' + possible_ext, delete=False)
- req = Request(file_reference, headers={'User-Agent': docassemble.base.config.daconfig.get('user agent', 'curl/7.64.0')})
+ req = Request(file_reference, headers={'User-Agent': daconfig.get('user agent', 'curl/7.64.0')})
response = urlopen(req)
temp_file.write(response.read())
# (local_filename, headers) = urllib.urlretrieve(file_reference)
@@ -147,7 +151,7 @@ def get_info_from_file_reference(file_reference, **kwargs):
elif manual_package is not None:
the_package = manual_package
if the_package is None:
- the_package = docassemble.base.functions.get_current_package()
+ the_package = get_current_package()
if folder is None:
m = re.search(r'^data/(templates|sources|static)/(.*)', file_reference)
if m:
@@ -165,7 +169,7 @@ def get_info_from_file_reference(file_reference, **kwargs):
result['package'] = the_package
elif len(parts) == 2:
result['package'] = parts[0]
- result['fullpath'] = docassemble.base.functions.static_filename_path(file_reference, return_nonexistent=return_nonexistent)
+ result['fullpath'] = static_filename_path(file_reference, return_nonexistent=return_nonexistent)
# logmessage("path is " + str(result['fullpath']))
if result['fullpath'] is not None: # os.path.isfile(result['fullpath'])
if not has_info:
@@ -230,14 +234,14 @@ def add_info_about_file(filename, basename, result):
try:
tree = ET.parse(filename)
root = tree.getroot()
- viewBox = root.attrib.get('viewBox', None)
- if viewBox is not None:
- dimen = viewBox.split(' ')
+ view_box = root.attrib.get('viewBox', None)
+ if view_box is not None:
+ dimen = view_box.split(' ')
if len(dimen) == 4:
result['width'] = float(dimen[2]) - float(dimen[0])
result['height'] = float(dimen[3]) - float(dimen[1])
- except:
- raise DAException("problem reading " + str(filename))
+ except Exception as err:
+ raise DAException("problem reading " + str(filename)) from err
# logmessage('add_info_about_file: could not read ' + str(filename))
@@ -245,7 +249,7 @@ def get_info_from_file_number(file_number, privileged=False, filename=None, uids
if current_user and current_user.is_authenticated and current_user.has_role('admin', 'developer', 'advocate', 'trainer'):
privileged = True
elif uids is None or len(uids) == 0:
- new_uid = docassemble.base.functions.get_uid()
+ new_uid = get_uid()
if new_uid is not None:
uids = [new_uid]
else:
diff --git a/docassemble_webapp/docassemble/webapp/files/file_number.py b/docassemble_webapp/docassemble/webapp/files/file_number.py
new file mode 100644
index 000000000..52afda170
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/files/file_number.py
@@ -0,0 +1,10 @@
+from docassemble.webapp.extensions import db
+from docassemble.webapp.hooks.impl import hookimpl
+from docassemble.webapp.main.models import Uploads
+
+@hookimpl
+def get_new_file_number(user_code, file_name, yaml_file_name):
+ new_upload = Uploads(key=user_code, filename=file_name, yamlfile=yaml_file_name)
+ db.session.add(new_upload)
+ db.session.commit()
+ return new_upload.indexno
diff --git a/docassemble_webapp/docassemble/webapp/files/helpers.py b/docassemble_webapp/docassemble/webapp/files/helpers.py
new file mode 100644
index 000000000..35f0385b1
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/files/helpers.py
@@ -0,0 +1,334 @@
+import json
+import shutil
+import re
+import tempfile
+import os
+from urllib.parse import quote as urllibquote
+import werkzeug
+from flask import redirect
+from flask_login import current_user
+from sqlalchemy import select, delete
+from docassemble.base.error import DAException
+from docassemble.base.functions import package_data_filename
+from docassemble.base.util import DAFile, DAFileList, DAFileCollection, DAStaticFile
+from docassemble.webapp.cloud.utils import cloud
+from docassemble.webapp.config import daconfig
+from docassemble.webapp.daredis import r
+from docassemble.webapp.extensions import db
+from docassemble.webapp.files.common import can_access_file_number
+from docassemble.webapp.hooks.impl import hookimpl
+from docassemble.webapp.main.models import UploadsRoleAuth, Uploads, UploadsUserAuth
+from docassemble.webapp.sessions import get_session_uids
+from docassemble.webapp.users.models import Role, UserModel
+from docassemble.webapp.utils.filenames import (
+ get_ext_and_mimetype,
+ secure_filename_unicode_ok,
+)
+from docassemble.webapp.utils.helpers import custom_send_file
+from docassemble.webapp.utils.logger import logmessage
+from .file_access import get_info_from_file_number, get_info_from_file_reference
+from .savedfile import SavedFile
+
+# @elapsed('file_set_attributes')
+@hookimpl
+def file_set_attributes(file_number, private, persistent, session, filename):
+ upload = db.session.execute(select(Uploads).filter_by(indexno=file_number).with_for_update()).scalar()
+ if upload is None:
+ db.session.commit()
+ raise DAException("file_set_attributes: file number " + str(file_number) + " not found.")
+ if private in [True, False] and upload.private != private:
+ upload.private = private
+ if persistent in [True, False] and upload.persistent != persistent:
+ upload.persistent = persistent
+ if isinstance(session, str):
+ upload.key = session
+ if isinstance(filename, str):
+ upload.filename = filename
+ db.session.commit()
+
+
+@hookimpl
+def file_user_access(file_number, allow_user_id, allow_email, disallow_user_id, disallow_email, disallow_all):
+ something_added = False
+ if allow_user_id:
+ for user_id in set(allow_user_id):
+ existing_user = db.session.execute(select(UserModel).filter_by(id=user_id)).first()
+ if not existing_user:
+ logmessage("file_user_access: invalid user ID " + repr(user_id))
+ continue
+ if db.session.execute(select(UploadsUserAuth).filter_by(uploads_indexno=file_number, user_id=user_id)).first():
+ continue
+ new_auth_record = UploadsUserAuth(uploads_indexno=file_number, user_id=user_id)
+ db.session.add(new_auth_record)
+ something_added = True
+ if something_added:
+ db.session.commit()
+ something_added = False
+ if allow_email:
+ for email in set(allow_email):
+ existing_user = db.session.execute(select(UserModel).filter_by(email=email)).first()
+ if not existing_user:
+ logmessage("file_user_access: invalid email " + repr(email))
+ continue
+ if db.session.execute(select(UploadsUserAuth).filter_by(uploads_indexno=file_number, user_id=existing_user.id)).first():
+ continue
+ new_auth_record = UploadsUserAuth(uploads_indexno=file_number, user_id=existing_user.id)
+ db.session.add(new_auth_record)
+ something_added = True
+ if something_added:
+ db.session.commit()
+ if disallow_user_id:
+ for user_id in set(disallow_user_id):
+ db.session.execute(delete(UploadsUserAuth).filter_by(uploads_indexno=file_number, user_id=user_id))
+ db.session.commit()
+ if disallow_email:
+ for email in set(disallow_email):
+ existing_user = db.session.execute(select(UserModel).filter_by(email=email)).scalar()
+ if not existing_user:
+ logmessage("file_user_access: invalid email " + repr(email))
+ continue
+ db.session.execute(delete(UploadsUserAuth).filter_by(uploads_indexno=file_number, user_id=existing_user.id))
+ db.session.commit()
+ if disallow_all:
+ db.session.execute(delete(UploadsUserAuth).filter_by(uploads_indexno=file_number))
+ if not (allow_user_id or allow_email or disallow_user_id or disallow_email or disallow_all):
+ result = {'user_ids': [], 'emails': [], 'temp_user_ids': []}
+ for auth in db.session.execute(select(UploadsUserAuth.user_id, UploadsUserAuth.temp_user_id, UserModel.email).outerjoin(UserModel, UploadsUserAuth.user_id == UserModel.id).where(UploadsUserAuth.uploads_indexno == file_number)).all():
+ if auth.user_id is not None:
+ result['user_ids'].append(auth.user_id)
+ if auth.temp_user_id is not None:
+ result['temp_user_ids'].append(auth.temp_user_id)
+ if auth.email:
+ result['emails'].append(auth.email)
+ return result
+ return None
+
+
+@hookimpl
+def file_privilege_access(file_number, allow, disallow, disallow_all):
+ something_added = False
+ if allow:
+ for privilege in set(allow):
+ existing_role = db.session.execute(select(Role).filter_by(name=privilege)).scalar_one()
+ if not existing_role:
+ logmessage("file_privilege_access: invalid privilege " + repr(privilege))
+ continue
+ if db.session.execute(select(UploadsRoleAuth).filter_by(uploads_indexno=file_number, role_id=existing_role.id)).first():
+ continue
+ new_auth_record = UploadsRoleAuth(uploads_indexno=file_number, role_id=existing_role.id)
+ db.session.add(new_auth_record)
+ something_added = True
+ if something_added:
+ db.session.commit()
+ if disallow:
+ for privilege in set(disallow):
+ existing_role = db.session.execute(select(Role).filter_by(name=privilege)).scalar_one()
+ if not existing_role:
+ logmessage("file_privilege_access: invalid privilege " + repr(privilege))
+ continue
+ db.session.execute(delete(UploadsRoleAuth).filter_by(uploads_indexno=file_number, role_id=existing_role.id))
+ db.session.commit()
+ if disallow_all:
+ db.session.execute(delete(UploadsRoleAuth).filter_by(uploads_indexno=file_number))
+ if not (allow or disallow or disallow_all):
+ result = []
+ for auth in db.session.execute(select(UploadsRoleAuth.id, Role.name).join(Role, UploadsRoleAuth.role_id == Role.id).where(UploadsRoleAuth.uploads_indexno == file_number)).all():
+ result.append(auth.name)
+ return result
+ return None
+
+@hookimpl(specname="file_finder")
+def file_finder(file_reference, question, folder, package, filename, return_nonexistent, uids):
+ return get_info_from_file_reference_with_uids(file_reference, question=question, folder=folder, package=package, filename=filename, return_nonexistent=return_nonexistent, uids=uids)
+
+def get_info_from_file_reference_with_uids(*pargs, **kwargs):
+ if 'uids' not in kwargs:
+ kwargs['uids'] = get_session_uids()
+ return get_info_from_file_reference(*pargs, **kwargs)
+
+
+@hookimpl(specname="file_number_finder")
+def get_info_from_file_number_with_uids(file_number, filename, uids, privileged):
+ if uids is None:
+ uids = get_session_uids()
+ return get_info_from_file_number(file_number, privileged=privileged, filename=filename, uids=uids)
+
+
+@hookimpl
+def path_from_reference(file_reference):
+ if isinstance(file_reference, DAFileCollection):
+ file_reference = file_reference._first_file()
+ if isinstance(file_reference, DAFileList):
+ file_reference = file_reference[0]
+ if isinstance(file_reference, DAFile):
+ file_info = get_info_from_file_number(file_reference.number, uids=get_session_uids())
+ if 'fullpath' not in file_info:
+ raise DAException("File not found")
+ friendly_path = os.path.join(tempfile.mkdtemp(prefix='SavedFile'), file_reference.filename)
+ try:
+ os.symlink(file_info['fullpath'], friendly_path)
+ except:
+ shutil.copyfile(file_info['fullpath'], friendly_path)
+ return friendly_path
+ if isinstance(file_reference, DAStaticFile):
+ return file_reference.path()
+ if file_reference is None:
+ return None
+ file_info = get_info_from_file_reference(file_reference)
+ if 'fullpath' not in file_info:
+ raise DAException("File not found")
+ return file_info['fullpath']
+
+
+def do_serve_stored_file(uid, number, filename, extension, download=False):
+ number = re.sub(r'[^0-9]', '', str(number))
+ if not can_access_file_number(number, uids=[uid]):
+ return ('File not found', 404)
+ try:
+ file_info = get_info_from_file_number(number, privileged=True, uids=get_session_uids())
+ except:
+ return ('File not found', 404)
+ if 'path' not in file_info:
+ return ('File not found', 404)
+ if not os.path.isfile(file_info['path']):
+ return ('File not found', 404)
+ response = custom_send_file(file_info['path'], mimetype=file_info['mimetype'], download_name=filename + '.' + extension)
+ if download:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(filename + '.' + extension))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+def do_serve_temporary_file(code, filename, extension, download=False):
+ file_info = r.get('da:tempfile:' + str(code))
+ if file_info is None:
+ logmessage("serve_temporary_file: file_info was none")
+ return ('File not found', 404)
+ (section, file_number) = file_info.decode().split('^')
+ the_file = SavedFile(file_number, fix=True, section=section)
+ the_path = the_file.path
+ if not os.path.isfile(the_path):
+ return ('File not found', 404)
+ (extension, mimetype) = get_ext_and_mimetype(filename + '.' + extension)
+ response = custom_send_file(the_path, mimetype=mimetype, download_name=filename + '.' + extension)
+ if download:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(filename + '.' + extension))
+ return response
+
+
+def do_serve_uploaded_file_with_filename_and_extension(number, filename, extension, download=False):
+ filename = secure_filename_unicode_ok(filename)
+ extension = werkzeug.utils.secure_filename(extension)
+ privileged = bool(current_user.is_authenticated and current_user.has_role('admin', 'advocate'))
+ number = re.sub(r'[^0-9]', '', str(number))
+ if cloud is not None and daconfig.get('use cloud urls', False):
+ if not (privileged or can_access_file_number(number, uids=get_session_uids())):
+ return ('File not found', 404)
+ the_file = SavedFile(number)
+ if download:
+ return redirect(the_file.temp_url_for(_attachment=True))
+ return redirect(the_file.temp_url_for())
+ try:
+ file_info = get_info_from_file_number(number, privileged=privileged, uids=get_session_uids())
+ except:
+ return ('File not found', 404)
+ if 'path' not in file_info:
+ return ('File not found', 404)
+ # logmessage("Filename is " + file_info['path'] + '.' + extension)
+ if os.path.isfile(file_info['path'] + '.' + extension):
+ # logmessage("Using " + file_info['path'] + '.' + extension)
+ extension, mimetype = get_ext_and_mimetype(file_info['path'] + '.' + extension)
+ response = custom_send_file(file_info['path'] + '.' + extension, mimetype=mimetype, download_name=filename + '.' + extension)
+ if download:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(filename + '.' + extension))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ if os.path.isfile(os.path.join(os.path.dirname(file_info['path']), filename + '.' + extension)):
+ # logmessage("Using " + os.path.join(os.path.dirname(file_info['path']), filename + '.' + extension))
+ extension, mimetype = get_ext_and_mimetype(filename + '.' + extension)
+ response = custom_send_file(os.path.join(os.path.dirname(file_info['path']), filename + '.' + extension), mimetype=mimetype, download_name=filename + '.' + extension)
+ if download:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(filename + '.' + extension))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ return ('File not found', 404)
+
+
+def do_serve_uploaded_file_with_extension(number, extension, download=False):
+ extension = werkzeug.utils.secure_filename(extension)
+ privileged = bool(current_user.is_authenticated and current_user.has_role('admin', 'advocate'))
+ number = re.sub(r'[^0-9]', '', str(number))
+ if cloud is not None and daconfig.get('use cloud urls', False):
+ if not can_access_file_number(number, uids=get_session_uids()):
+ return ('File not found', 404)
+ the_file = SavedFile(number)
+ if download:
+ return redirect(the_file.temp_url_for(_attachment=True))
+ return redirect(the_file.temp_url_for())
+ try:
+ file_info = get_info_from_file_number(number, privileged=privileged, uids=get_session_uids())
+ except:
+ return ('File not found', 404)
+ if 'path' not in file_info:
+ return ('File not found', 404)
+ if os.path.isfile(file_info['path'] + '.' + extension):
+ extension, mimetype = get_ext_and_mimetype(file_info['path'] + '.' + extension)
+ response = custom_send_file(file_info['path'] + '.' + extension, mimetype=mimetype, download_name=str(number) + '.' + extension)
+ if download:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(str(number) + '.' + extension))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ return ('File not found', 404)
+
+
+def do_serve_uploaded_file(number, download=False):
+ number = re.sub(r'[^0-9]', '', str(number))
+ privileged = bool(current_user.is_authenticated and current_user.has_role('admin', 'advocate'))
+ try:
+ file_info = get_info_from_file_number(number, privileged=privileged, uids=get_session_uids())
+ except:
+ return ('File not found', 404)
+ if 'path' not in file_info:
+ return ('File not found', 404)
+ if not os.path.isfile(file_info['path']):
+ return ('File not found', 404)
+ response = custom_send_file(file_info['path'], mimetype=file_info['mimetype'], download_name=os.path.basename(file_info['path']))
+ if download:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(os.path.basename(file_info['path'])))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+
+
+def do_serve_uploaded_page(number, page, download=False, size='page'):
+ number = re.sub(r'[^0-9]', '', str(number))
+ page = re.sub(r'[^0-9]', '', str(page))
+ privileged = bool(current_user.is_authenticated and current_user.has_role('admin', 'advocate'))
+ try:
+ file_info = get_info_from_file_number(number, privileged=privileged, uids=get_session_uids())
+ except BaseException as err:
+ logmessage("do_serve_uploaded_page: " + err.__class__.__name__ + str(err))
+ return ('File not found', 404)
+ if 'path' not in file_info:
+ logmessage('serve_uploaded_page: no access to file number ' + str(number))
+ return ('File not found', 404)
+ try:
+ the_file = DAFile(mimetype=file_info['mimetype'], extension=file_info['extension'], number=number, make_thumbnail=page)
+ filename = the_file.page_path(page, size)
+ assert filename is not None
+ except BaseException as err:
+ logmessage("Could not make thumbnail: " + err.__class__.__name__ + ": " + str(err))
+ filename = None
+ if filename is None:
+ logmessage("do_serve_uploaded_page: sending blank image")
+ the_file = package_data_filename('docassemble.base:data/static/blank_page.png')
+ response = custom_send_file(the_file, mimetype='image/png', download_name='blank_page.png')
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ if os.path.isfile(filename):
+ response = custom_send_file(filename, mimetype='image/png', download_name=os.path.basename(filename))
+ if download:
+ response.headers['Content-Disposition'] = 'attachment; filename=' + json.dumps(urllibquote(os.path.basename(filename)))
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response
+ logmessage('do_serve_uploaded_page: path ' + filename + ' is not a file')
+ return ('File not found', 404)
diff --git a/docassemble_webapp/docassemble/webapp/files/hooks.py b/docassemble_webapp/docassemble/webapp/files/hooks.py
new file mode 100644
index 000000000..0efb7a5e6
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/files/hooks.py
@@ -0,0 +1,28 @@
+from docassemble.base.error import DAException
+from docassemble.base.functions import get_uid
+from docassemble.webapp.files.file_number import get_new_file_number
+from docassemble.webapp.hooks.impl import hookimpl
+from docassemble.webapp.interview.helpers import unattached_uid
+from docassemble.webapp.utils.filenames import get_ext_and_mimetype
+from .savedfile import SavedFile
+
+@hookimpl
+def save_numbered_file(filename, orig_path, yaml_file_name, uid):
+ if uid is None:
+ try:
+ uid = get_uid()
+ assert uid is not None
+ except:
+ uid = unattached_uid()
+ if uid is None:
+ raise DAException("save_numbered_file: uid not defined")
+ file_number = get_new_file_number(uid, filename, yaml_file_name)
+ extension, mimetype = get_ext_and_mimetype(filename)
+ new_file = SavedFile(file_number, extension=extension, fix=True, should_not_exist=True)
+ new_file.copy_from(orig_path)
+ new_file.save(finalize=True)
+ return (file_number, extension, mimetype)
+
+@hookimpl
+def get_saved_file_class():
+ return SavedFile
diff --git a/docassemble_webapp/docassemble/webapp/package_info.py b/docassemble_webapp/docassemble/webapp/files/package_info.py
similarity index 89%
rename from docassemble_webapp/docassemble/webapp/package_info.py
rename to docassemble_webapp/docassemble/webapp/files/package_info.py
index 7659cbf24..0f21e6643 100644
--- a/docassemble_webapp/docassemble/webapp/package_info.py
+++ b/docassemble_webapp/docassemble/webapp/files/package_info.py
@@ -1,6 +1,6 @@
-from docassemble.webapp.db_object import db
-from docassemble.webapp.packages.models import Package
from sqlalchemy import select
+from docassemble.webapp.extensions import db
+from docassemble.webapp.packages.models import Package
def retrieve_package_info(package_name):
diff --git a/docassemble_webapp/docassemble/webapp/files.py b/docassemble_webapp/docassemble/webapp/files/savedfile.py
similarity index 93%
rename from docassemble_webapp/docassemble/webapp/files.py
rename to docassemble_webapp/docassemble/webapp/files/savedfile.py
index 884c13775..03a2a8c28 100644
--- a/docassemble_webapp/docassemble/webapp/files.py
+++ b/docassemble_webapp/docassemble/webapp/files/savedfile.py
@@ -1,35 +1,34 @@
import datetime
import json
-import mimetypes
import os
import re
import shutil
import subprocess
# import sys
import tempfile
+import urllib.parse
import zipfile
import tomli_w
-import urllib.parse
+import requests
from packaging import version
from flask import url_for
from flask_login import current_user
try:
import zoneinfo
except ImportError:
- from backports import zoneinfo
-import requests
-from docassemble.base.config import daconfig
+ from backports import zoneinfo # type: ignore[no-redef]
from docassemble.base.error import DAError
from docassemble.base.generate_key import random_alphanumeric
-from docassemble.base.logger import logmessage
+from docassemble.base.thread_context import this_thread
+from docassemble.webapp.cloud.utils import cloud
+from docassemble.webapp.config import UPLOAD_DIRECTORY, daconfig
+from docassemble.webapp.daredis import r
+from docassemble.webapp.files.package_info import retrieve_package_info
+from docassemble.webapp.packages.pip import get_pip_info
+from docassemble.webapp.utils.filenames import get_ext_and_mimetype, directory_for
+from docassemble.webapp.utils.logger import logmessage
+from docassemble.webapp.utils.regex import url_sanitize
import docassemble.webapp.spdx
-import docassemble.base.functions
-from docassemble.webapp.update import get_pip_info
-import docassemble.webapp.cloud
-
-cloud = docassemble.webapp.cloud.get_cloud()
-
-UPLOAD_DIRECTORY = daconfig.get('uploads', '/usr/share/docassemble/files')
DEFAULT_GITIGNORE = """\
__pycache__/
@@ -100,20 +99,16 @@ def path_to_key(path):
return '/'.join(str(path).split(os.sep))
-def url_sanitize(url):
- return re.sub(r'\s', ' ', url)
-
-
class SavedFile:
def __init__(self, file_number, extension=None, fix=False, section='files', filename='file', subdir=None, should_not_exist=False, must_exist=False): # pylint: disable=too-many-positional-arguments
file_number = int(file_number)
section = str(section)
- if section not in docassemble.base.functions.this_thread.saved_files:
- docassemble.base.functions.this_thread.saved_files[section] = {}
- if file_number in docassemble.base.functions.this_thread.saved_files[section]:
+ if section not in this_thread.saved_files:
+ this_thread.saved_files[section] = {}
+ if file_number in this_thread.saved_files[section]:
# logmessage("SavedFile: using cache for " + section + '/' + str(file_number))
- sf = docassemble.base.functions.this_thread.saved_files[section][file_number]
+ sf = this_thread.saved_files[section][file_number]
for attribute in ['file_number', 'fixed', 'section', 'filename', 'extension', 'directory', 'path', 'modtimes', 'keydict', 'subdir']:
if hasattr(sf, attribute):
setattr(self, attribute, getattr(sf, attribute))
@@ -136,7 +131,7 @@ def __init__(self, file_number, extension=None, fix=False, section='files', file
self.directory = os.path.join(UPLOAD_DIRECTORY, str(self.section), str(file_number))
else:
self.directory = os.path.join(tempfile.gettempdir(), str(self.section), str(self.file_number))
- docassemble.base.functions.this_thread.saved_files[section][file_number] = self
+ this_thread.saved_files[section][file_number] = self
if self.subdir and self.subdir != '' and self.subdir != 'default':
self.path = os.path.join(self.directory, self.subdir, self.filename)
else:
@@ -258,7 +253,7 @@ def delete(self):
pass
if hasattr(self, 'directory') and os.path.isdir(self.directory):
shutil.rmtree(self.directory)
- del docassemble.base.functions.this_thread.saved_files[str(self.section)][int(self.file_number)]
+ del this_thread.saved_files[str(self.section)][int(self.file_number)]
def save(self, finalize=False):
self.fix()
@@ -276,23 +271,23 @@ def fetch_url(self, url, **kwargs):
filename = kwargs.get('filename', self.filename)
self.fix()
try:
- with requests.get(url, stream=True, timeout=60) as r:
- r.raise_for_status()
+ with requests.get(url, stream=True, timeout=60) as rr:
+ rr.raise_for_status()
with open(os.path.join(self.directory, filename), 'wb') as fp:
- for chunk in r.iter_content(8192):
+ for chunk in rr.iter_content(8192):
fp.write(chunk)
except requests.exceptions.HTTPError as err:
- raise DAError("from_url: Error %s" % (str(err),))
+ raise DAError("from_url: Error %s" % (str(err),)) from err
self.save()
def fetch_url_post(self, url, post_args, **kwargs):
filename = kwargs.get('filename', self.filename)
self.fix()
- r = requests.post(url_sanitize(url), data=post_args, timeout=600)
- if r.status_code != 200:
+ rr = requests.post(url_sanitize(url), data=post_args, timeout=600)
+ if rr.status_code != 200:
raise DAError('fetch_url_post: retrieval from ' + url + 'failed')
with open(os.path.join(self.directory, filename), 'wb') as fp:
- for block in r.iter_content(8192):
+ for block in rr.iter_content(8192):
fp.write(block)
self.save()
@@ -394,15 +389,14 @@ def temp_url_for(self, **kwargs):
return key.generate_url(seconds, display_filename=kwargs.get('display_filename', None), inline=inline, content_type=kwargs.get('content_type', None))
logmessage("key " + str(keyname) + " did not exist")
return 'about:blank'
- r = docassemble.base.functions.server.server_redis
while True:
code = random_alphanumeric(32)
keyname = 'da:tempfile:' + code
if r.setnx(keyname, str(self.section) + '^' + str(self.file_number)):
r.expire(keyname, seconds)
break
- use_external = kwargs.get('_external', bool('jsembed' in docassemble.base.functions.this_thread.misc))
- url = url_for('rootindex', _external=use_external).rstrip('/')
+ use_external = kwargs.get('_external', bool('jsembed' in this_thread.misc))
+ url = url_for('interview.rootindex', _external=use_external).rstrip('/')
url += '/tempfile' + suffix + '/' + code + '/' + urllib.parse.quote(path_to_key(kwargs.get('display_filename', filename)))
return url
@@ -443,8 +437,8 @@ def url_for(self, **kwargs):
suffix = 'download'
else:
suffix = ''
- use_external = kwargs.get('_external', bool('jsembed' in docassemble.base.functions.this_thread.misc))
- base_url = url_for('rootindex', _external=use_external).rstrip('/')
+ use_external = kwargs.get('_external', bool('jsembed' in this_thread.misc))
+ base_url = url_for('interview.rootindex', _external=use_external).rstrip('/')
if extn is None:
extn = ''
else:
@@ -512,21 +506,6 @@ def finalize(self):
# logmessage("finalize: ending " + str(self.section) + '/' + str(self.file_number))
-def get_ext_and_mimetype(filename):
- mimetype, encoding = mimetypes.guess_type(filename) # pylint: disable=unused-variable
- extension = filename.lower()
- extension = re.sub(r'.*\.', '', extension)
- if extension == "jpeg":
- extension = "jpg"
- if extension == "tiff":
- extension = "tif"
- if extension == '3gpp':
- mimetype = 'audio/3gpp'
- if extension in ('yaml', 'yml'):
- mimetype = 'text/plain'
- return (extension, mimetype)
-
-
def publish_package(pkgname, info, author_info, current_project='default'):
directory = make_package_dir(pkgname, info, author_info, current_project=current_project)
packagedir = os.path.join(directory, 'docassemble-' + str(pkgname))
@@ -574,7 +553,6 @@ def make_package_zip(pkgname, info, author_info, tz_name, current_project='defau
def get_package_identifier(package_name):
- from docassemble.webapp.package_info import retrieve_package_info # pylint: disable=import-outside-toplevel
package_info = retrieve_package_info(package_name)
logmessage("package_info is " + repr(package_info))
if package_info is not None and package_info['type'] == 'git':
@@ -592,9 +570,9 @@ def get_package_identifier(package_name):
latest_release = None
printable_latest_release = None
try:
- r = requests.get("https://pypi.org/pypi/%s/json" % package_name, timeout=5)
- assert r.status_code == 200
- pypi_info = r.json()
+ rr = requests.get("https://pypi.org/pypi/%s/json" % package_name, timeout=5)
+ assert rr.status_code == 200
+ pypi_info = rr.json()
for the_version in pypi_info['releases'].keys():
past_version = version.parse(the_version)
if past_version <= installed_version and (latest_release is None or past_version > latest_release):
@@ -852,12 +830,6 @@ def find_package_data(where='.', package='', exclude=standard_exclude, exclude_d
return directory
-def directory_for(area, current_project):
- if current_project == 'default':
- return area.directory
- return os.path.join(area.directory, current_project)
-
-
def update_access_time(filepath):
with open(filepath, "rb") as fp:
fp.seek(0, 0)
diff --git a/docassemble_webapp/docassemble/webapp/files/views.py b/docassemble_webapp/docassemble/webapp/files/views.py
new file mode 100644
index 000000000..d88a7d679
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/files/views.py
@@ -0,0 +1,73 @@
+from .helpers import (
+ do_serve_uploaded_page,
+ do_serve_uploaded_file_with_filename_and_extension,
+ do_serve_temporary_file,
+ do_serve_uploaded_file_with_extension,
+ do_serve_stored_file,
+ do_serve_uploaded_file,
+)
+from .blueprint import files_bp
+
+@files_bp.route('/storedfile///.', methods=['GET'])
+def serve_stored_file(uid, number, filename, extension):
+ return do_serve_stored_file(uid, number, filename, extension)
+
+
+@files_bp.route('/storedfiledownload///.', methods=['GET'])
+def serve_stored_file_download(uid, number, filename, extension):
+ return do_serve_stored_file(uid, number, filename, extension, download=True)
+
+
+@files_bp.route('/tempfile//.', methods=['GET'])
+def serve_temporary_file(code, filename, extension):
+ return do_serve_temporary_file(code, filename, extension)
+
+
+@files_bp.route('/tempfiledownload//.', methods=['GET'])
+def serve_temporary_file_download(code, filename, extension):
+ return do_serve_temporary_file(code, filename, extension, download=True)
+
+
+@files_bp.route('/uploadedfile//.', methods=['GET'])
+def serve_uploaded_file_with_filename_and_extension(number, filename, extension):
+ return do_serve_uploaded_file_with_filename_and_extension(number, filename, extension)
+
+
+@files_bp.route('/uploadedfiledownload//.', methods=['GET'])
+def serve_uploaded_file_with_filename_and_extension_download(number, filename, extension):
+ return do_serve_uploaded_file_with_filename_and_extension(number, filename, extension, download=True)
+
+
+@files_bp.route('/uploadedfile/.', methods=['GET'])
+def serve_uploaded_file_with_extension(number, extension):
+ return do_serve_uploaded_file_with_extension(number, extension)
+
+
+@files_bp.route('/uploadedfiledownload/.', methods=['GET'])
+def serve_uploaded_file_with_extension_download(number, extension):
+ return do_serve_uploaded_file_with_extension(number, extension, download=True)
+
+
+@files_bp.route('/uploadedfile/', methods=['GET'])
+def serve_uploaded_file(number):
+ return do_serve_uploaded_file(number)
+
+
+@files_bp.route('/uploadedpage//', methods=['GET'])
+def serve_uploaded_page(number, page):
+ return do_serve_uploaded_page(number, page, size='page')
+
+
+@files_bp.route('/uploadedpagedownload//', methods=['GET'])
+def serve_uploaded_page_download(number, page):
+ return do_serve_uploaded_page(number, page, download=True, size='page')
+
+
+@files_bp.route('/uploadedpagescreen//', methods=['GET'])
+def serve_uploaded_pagescreen(number, page):
+ return do_serve_uploaded_page(number, page, size='screen')
+
+
+@files_bp.route('/uploadedpagescreendownload//', methods=['GET'])
+def serve_uploaded_pagescreen_download(number, page):
+ return do_serve_uploaded_page(number, page, download=True, size='screen')
diff --git a/docassemble_webapp/docassemble/webapp/fix_postgresql_tables.py b/docassemble_webapp/docassemble/webapp/fix_postgresql_tables.py
index 9c048a2ba..88d41e83f 100755
--- a/docassemble_webapp/docassemble/webapp/fix_postgresql_tables.py
+++ b/docassemble_webapp/docassemble/webapp/fix_postgresql_tables.py
@@ -1,12 +1,10 @@
+# pylint: disable=wrong-import-position
import os
import sys
from pathlib import Path
import importlib.resources
import psycopg2
-import docassemble.base.config
-if __name__ == "__main__":
- docassemble.base.config.load(arguments=sys.argv)
-from docassemble.base.config import daconfig
+from docassemble.webapp.config import daconfig
from docassemble.base.logger import logmessage
@@ -95,19 +93,19 @@ def main():
commands = []
if db_table_prefix + 'shortener' in existing_columns and db_table_prefix + 'email' not in existing_columns:
commands.append("drop table if exists " + db_table_prefix + "shortener;")
- for table_name in desired_columns:
+ for table_name, table_info in desired_columns.items():
if db_table_prefix + table_name in existing_columns:
- for column_name in desired_columns[table_name]:
+ for column_name in table_info:
if column_name not in existing_columns[db_table_prefix + table_name]:
- output = "alter table \"" + db_table_prefix + table_name + "\" add column \"" + column_name + "\" " + desired_columns[table_name][column_name]['type']
- if desired_columns[table_name][column_name]['size']:
- output += "(" + desired_columns[table_name][column_name]['size'] + ")"
- if desired_columns[table_name][column_name]['default']:
- output += " default " + desired_columns[table_name][column_name]['default']
+ output = "alter table \"" + db_table_prefix + table_name + "\" add column \"" + column_name + "\" " + table_info[column_name]['type']
+ if table_info[column_name]['size']:
+ output += "(" + table_info[column_name]['size'] + ")"
+ if table_info[column_name]['default']:
+ output += " default " + table_info[column_name]['default']
output += ";"
commands.append(output)
- if len(commands):
+ if len(commands) > 0:
for command in commands:
try:
cur.execute(command)
diff --git a/docassemble_webapp/docassemble/webapp/fixup.sh b/docassemble_webapp/docassemble/webapp/fixup.sh
new file mode 100755
index 000000000..45de33974
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/fixup.sh
@@ -0,0 +1,27 @@
+#! /bin/bash
+
+dirs="admin daglobal files mail objectstore sms users api develop interview main packages translation auth emailserver jsonstorage ml phonelogin tts core fax logs monitor react twilio"
+
+for dir in $dirs; do
+ if [ "$dir" = "blueprints" ]; then
+ continue
+ fi
+ name=$(basename "$dir")
+ if [ ! -f "$dir/__init__.py" ]; then
+ read -r -d '' contents < "$dir/__init__.py"
+ fi
+ if [ -f "$dir/views.py" ]; then
+ sed -i 's/^@app\./@'${name}'_bp./' "$dir/views.py"
+ fi
+done
diff --git a/docassemble_webapp/docassemble/webapp/flask_app.py b/docassemble_webapp/docassemble/webapp/flask_app.py
new file mode 100644
index 000000000..9e5f7808d
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/flask_app.py
@@ -0,0 +1,4 @@
+from docassemble.webapp.app_object import flaskapp
+from docassemble.webapp.app_initialize import init_app
+
+init_app(flaskapp)
diff --git a/docassemble_webapp/docassemble/webapp/flask_sql_config.py b/docassemble_webapp/docassemble/webapp/flask_sql_config.py
new file mode 100644
index 000000000..941bf5560
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/flask_sql_config.py
@@ -0,0 +1,81 @@
+# pylint: disable=global-statement
+# import sqlalchemy
+# from flask_sqlalchemy import SQLAlchemy as _BaseSQLAlchemy
+# from sqlalchemy.orm import declarative_base
+# from sqlalchemy.orm import sessionmaker, relationship, backref
+# import docassemble_flask_user
+
+def init_app(app, other_databases=False):
+ import docassemble.webapp.database
+ from docassemble.webapp.config import daconfig
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+ alchemy_connect_string = docassemble.webapp.database.alchemy_connection_string()
+ app.config['SQLALCHEMY_DATABASE_URI'] = alchemy_connect_string
+ app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
+ "pool_pre_ping": docassemble.webapp.database.pool_pre_ping,
+ }
+ if alchemy_connect_string.startswith('postgres'):
+ app.config['SQLALCHEMY_ENGINE_OPTIONS']['connect_args'] = docassemble.webapp.database.connect_args()
+ if other_databases and daconfig.get('variables snapshot db') is not None:
+ import docassemble.webapp.user_database
+ snapshot_url = docassemble.webapp.user_database.alchemy_url('variables snapshot db')
+ snapshot_bind = {'url': snapshot_url, 'pool_pre_ping': daconfig.get('sql ping', False)}
+ snapshot_connect_args = docassemble.webapp.user_database.connect_args('variables snapshot db')
+ if snapshot_connect_args:
+ snapshot_bind['connect_args'] = snapshot_connect_args
+ app.config['SQLALCHEMY_BINDS'] = {'variables_snapshot': snapshot_bind}
+
+# Am I getting rid of this?
+
+# db = None
+# UserMixin = None # pylint: disable=invalid-name
+
+# def init_flask():
+# global db
+# global UserMixin
+# import docassemble.webapp.database
+# if docassemble.webapp.database.pool_pre_ping:
+# class SQLAlchemy(_BaseSQLAlchemy):
+# def apply_pool_defaults(self, app, options):
+# super().apply_pool_defaults(app, options)
+# options["pool_pre_ping"] = True
+# options["future"] = True
+# return options
+# else:
+# class SQLAlchemy(_BaseSQLAlchemy):
+# def apply_pool_defaults(self, app, options):
+# super().apply_pool_defaults(app, options)
+# options["future"] = True
+# return options
+# db = SQLAlchemy()
+# UserMixin = docassemble_flask_user.UserMixin
+# return db
+
+# # Am I getting rid of this?
+
+# def init_sqlalchemy():
+# global db
+# global UserMixin
+# import docassemble.webapp.database
+# url = docassemble.webapp.database.alchemy_connection_string()
+# if url.startswith('postgresql'):
+# connect_args = docassemble.webapp.database.connect_args()
+# db = sqlalchemy.create_engine(url, client_encoding='utf8', connect_args=connect_args, pool_pre_ping=docassemble.webapp.database.pool_pre_ping)
+# else:
+# db = sqlalchemy.create_engine(url, pool_pre_ping=docassemble.webapp.database.pool_pre_ping)
+# # meta = sqlalchemy.MetaData(bind=con, reflect=True)
+# Session = sessionmaker(bind=db)
+# db.Model = declarative_base()
+# db.Column = sqlalchemy.Column
+# db.Integer = sqlalchemy.Integer
+# db.String = sqlalchemy.String
+# db.Boolean = sqlalchemy.Boolean
+# db.Text = sqlalchemy.Text
+# db.DateTime = sqlalchemy.DateTime
+# db.func = sqlalchemy.func
+# db.relationship = relationship
+# db.backref = backref
+# db.ForeignKey = sqlalchemy.ForeignKey
+# db.session = Session()
+# UserMixin = object
+# return db
diff --git a/docassemble_webapp/docassemble/webapp/google_api.py b/docassemble_webapp/docassemble/webapp/google_api.py
index 4c4cd5b0a..dd4778307 100644
--- a/docassemble_webapp/docassemble/webapp/google_api.py
+++ b/docassemble_webapp/docassemble/webapp/google_api.py
@@ -4,50 +4,50 @@
from google.oauth2 import service_account
import google.cloud.storage
import google.cloud.vision
-from docassemble.base.config import daconfig
-from docassemble.base.error import DAException
from oauth2client.service_account import ServiceAccountCredentials
+from docassemble.base.error import DAException
+from docassemble.webapp.config import daconfig
logging.getLogger('googleapiclient.discovery_cache').setLevel(logging.ERROR)
credential_json = daconfig.get('google').get('service account credentials', None)
if credential_json is None:
- credential_info = None
+ CREDENTIAL_INFO = None
else:
try:
- credential_info = json.loads(credential_json, strict=False)
+ CREDENTIAL_INFO = json.loads(credential_json, strict=False)
except BaseException as err:
- credential_info = None
+ CREDENTIAL_INFO = None
sys.stderr.write("Unable to load google service account credentials:\n")
sys.stderr.write(str(err) + "\n")
def google_api_credentials(scope):
"""Returns an OAuth2 credentials object for the given scope."""
- if credential_info is None:
+ if CREDENTIAL_INFO is None:
raise DAException("google service account credentials not defined in configuration")
if scope is None:
scope = ['https://www.googleapis.com/auth/drive']
if not isinstance(scope, list):
scope = [scope]
- return ServiceAccountCredentials.from_json_keyfile_dict(credential_info, scope)
+ return ServiceAccountCredentials.from_json_keyfile_dict(CREDENTIAL_INFO, scope)
def google_cloud_credentials(scopes=None):
"""Returns google.oauth2.service_account.Credentials that can be used with the google.cloud API."""
- if credential_info is None:
+ if CREDENTIAL_INFO is None:
raise DAException("google service account credentials not defined in configuration")
- return service_account.Credentials.from_service_account_info(credential_info, scopes=scopes)
+ return service_account.Credentials.from_service_account_info(CREDENTIAL_INFO, scopes=scopes)
def project_id():
"""Returns the project ID as defined in the google service account credentials in the Configuration."""
- return credential_info['project_id']
+ return CREDENTIAL_INFO['project_id']
def google_cloud_storage_client():
"""Returns a Client object for google.cloud.storage."""
- return google.cloud.storage.Client(credentials=google_cloud_credentials(), project=credential_info['project_id'])
+ return google.cloud.storage.Client(credentials=google_cloud_credentials(), project=CREDENTIAL_INFO['project_id'])
def google_cloud_vision_client():
diff --git a/docassemble_webapp/docassemble/webapp/hooks/__init__.py b/docassemble_webapp/docassemble/webapp/hooks/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/docassemble_webapp/docassemble/webapp/hooks/default.py b/docassemble_webapp/docassemble/webapp/hooks/default.py
new file mode 100644
index 000000000..6ba0a395b
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/hooks/default.py
@@ -0,0 +1,480 @@
+# pylint: disable=unused-argument
+from typing import Any
+from .impl import hookimpl
+
+@hookimpl(trylast=True)
+def get_default_language() -> str:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_default_dialect() -> str:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_default_locale() -> str:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_default_voice() -> str:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_default_timezone() -> str:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_default_country() -> str:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_configuration() -> dict:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_hostname() -> str:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_debug_status() -> bool:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def save_numbered_file(filename, orig_path, yaml_file_name, uid) -> tuple:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def send_mail(the_message, config) -> None:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def absolute_filename(the_file) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def write_record(key, data) -> int:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def read_records(key) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def delete_record(key, the_id) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def generate_csrf(secret_key, token_key) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def url_for(endpoint, kwargs) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_new_file_number(user_code, file_name, yaml_file_name) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_ext_and_mimetype(filename) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def file_finder(file_reference, question, folder, package, filename, return_nonexistent, uids) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def file_number_finder(file_number, filename, uids, privileged) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_sql_get(key, secret) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_sql_defined(key) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_sql_set(key, val, encrypted, secret, the_user_id) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_sql_delete(key) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_sql_keys(prefix) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def alchemy_url(db_config) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def connect_args(db_config) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_default_table_class() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_default_thead_class() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def to_text(html_doc) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def url_finder(file_reference, kwargs) -> Any:
+ """Find a URL for a file reference; kwargs is a dict of keyword arguments"""
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def navigation_bar(nav, interview, wrapper, inner_div_class, inner_div_extra, show_links, hide_inactive_subs, a_class, show_nesting, include_arrows, always_open, return_dict) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def chat_partners_available(session_id, yaml_filename, the_user_id, mode, partner_roles) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_chat_log(yaml_filename, session_id, secret, utc, timezone) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def sms_body(phone_number, body, config) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def send_fax(fax_number, the_file, config, country) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_sms_session(phone_number, config) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def initiate_sms_session(phone_number, yaml_filename, uid, secret, encrypted, user_id, email, new, config) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def terminate_sms_session(phone_number, config) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def applock(action, application, maxtime) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_twilio_config() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_server_redis() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_server_redis_user() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_user_object(user_id) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def user_id_dict() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def retrieve_email(email_id) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def retrieve_emails(kwargs) -> Any:
+ """Retrieve emails; kwargs is a dict of keyword arguments"""
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_short_code(kwargs) -> Any:
+ """Get short code; kwargs is a dict of keyword arguments"""
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def make_png_for_pdf(doc, prefix, page) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def ocr_google_in_background(image_file, raw_result, user_code) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def task_ready(task_id) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def wait_for_task(task_id, timeout) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def user_interviews(user_id, secret, exclude_invalid, action, filename, session, tag, include_dict, delete_shared, admin, start_id, temp_user_id, query, minimal) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_interview_menu(absolute_urls, start_new, tag) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_get_user_list(include_inactive, start_id) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_get_user_info(user_id, email, case_sensitive, admin) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_set_user_info(kwargs) -> Any:
+ """Set user info; kwargs is a dict of keyword arguments"""
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def make_user_inactive(user_id, email) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_get_secret(username, password, case_sensitive) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_get_session_variables(yaml_filename, session_id, secret, simplify, use_lock) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_go_back_in_session(yaml_filename, session_id, secret, return_question, use_lock, encode) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_create_session(yaml_filename, secret, url_args, referer, req) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_set_session_variables(yaml_filename, session_id, variables, secret, return_question, literal_variables, del_variables, question_name, event_list, advance_progress_meter, post_setting, use_lock, encode, process_objects) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_privileges_list(admin) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def add_privilege(privilege) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def remove_privilege(privilege) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def add_user_privilege(user_id, privilege) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def remove_user_privilege(user_id, privilege) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_permissions_of_privilege(privilege, privileged) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_create_user(email, password, privileges, info) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def file_set_attributes(file_number, private, persistent, session, filename) -> Any:
+ """Set attributes on a stored file"""
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def file_user_access(file_number, allow_user_id, allow_email, disallow_user_id, disallow_email, disallow_all) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def file_privilege_access(file_number, allow, disallow, disallow_all) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def fg_make_png_for_pdf(doc, prefix, page) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def fg_make_png_for_pdf_path(path, prefix, page) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def fg_make_pdf_for_word_path(path, extension) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_get_question_data(yaml_filename, session_id, secret, use_lock, user_dict, steps, is_encrypted, old_user_dict, save, post_setting, advance_progress_meter, action, encode) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def fix_pickle_obj(data) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_main_page_parts() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_saved_file_class() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def path_from_reference(file_reference) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_button_class_prefix() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def write_answer_json(user_code, filename, data, tags, persistent) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def read_answer_json(user_code, filename, tags, all_tags) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def delete_answer_json(user_code, filename, tags, delete_all, delete_persistent) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def variables_snapshot_connection() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def variables_snapshot_connect() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_referer() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def stash_data(data, expire) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def retrieve_stashed_data(key, secret, delete, refresh) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def secure_filename_spaces_ok(filename) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def secure_filename_unicode_ok(the_filename) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def secure_filename(filename) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def transform_json_variables(obj) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_login_url(kwargs) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_run_action_in_session(kwargs) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def server_invite_user(email_address, privilege, send) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_url() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def release_lock(user_code, filename) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def register_db(db_name) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def create_objects_in_db(db_name) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_cloud() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def cloud_custom(provider, config) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def google_api() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_mail_class() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_celery_app() -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def get_task(obj) -> Any:
+ raise NotImplementedError("Not implemented")
+
+@hookimpl(trylast=True)
+def chord(arg) -> Any:
+ raise NotImplementedError("Not implemented")
+
+# @hookimpl(trylast=True)
+# def fix_ml_files(playground_number, current_project) -> Any:
+# return None
+
+# @hookimpl(trylast=True)
+# def write_ml_source(playground, playground_number, current_project, filename, finalize) -> Any:
+# return None
+
+# @hookimpl(trylast=True)
+# def ensure_training_loaded(interview) -> Any:
+# return None
+
+# @hookimpl(trylast=True)
+# def manage_chat_logs(mode: int, kwargs: dict) -> None:
+# return None
+
+# @hookimpl(trylast=True)
+# def manage_global_objects(mode: int, kwargs: dict) -> None:
+# return None
+
+# @hookimpl(trylast=True)
+# def manage_email_server_objects(mode: int, kwargs: dict) -> None:
+# return None
+
+# @hookimpl(trylast=True)
+# def manage_tts_objects(mode: int, kwargs: dict) -> None:
+# return None
+
+@hookimpl(trylast=True)
+def get_chat_log_internal(chat_mode, yaml_filename, session_id, user_id, temp_user_id, secret, self_user_id, self_temp_id) -> list:
+ return []
+
+@hookimpl(trylast=True)
+def get_ml_info(varname, default_package, default_file) -> Any:
+ raise NotImplementedError("Not implemented")
diff --git a/docassemble_webapp/docassemble/webapp/hooks/impl.py b/docassemble_webapp/docassemble/webapp/hooks/impl.py
new file mode 100644
index 000000000..b702d8598
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/hooks/impl.py
@@ -0,0 +1,3 @@
+import pluggy
+
+hookimpl = pluggy.HookimplMarker("docassemble")
diff --git a/docassemble_webapp/docassemble/webapp/install_certs.py b/docassemble_webapp/docassemble/webapp/install_certs.py
index cbeb078e6..91ca53b25 100644
--- a/docassemble_webapp/docassemble/webapp/install_certs.py
+++ b/docassemble_webapp/docassemble/webapp/install_certs.py
@@ -1,3 +1,4 @@
+# pylint: disable=wrong-import-position
import sys
import os
import stat
@@ -8,10 +9,16 @@
if __name__ == "__main__":
import docassemble.base.config
docassemble.base.config.load(arguments=sys.argv)
-from docassemble.base.config import daconfig, S3_ENABLED, s3_config, AZURE_ENABLED, azure_config
-from docassemble.base.logger import logmessage
import docassemble.base.amazon
import docassemble.base.microsoft
+from docassemble.webapp.config import (
+ daconfig,
+ S3_ENABLED,
+ s3_config,
+ AZURE_ENABLED,
+ azure_config,
+)
+from docassemble.webapp.utils.logger import logmessage
def main():
@@ -21,26 +28,26 @@ def main():
if S3_ENABLED:
my_config = copy.deepcopy(s3_config)
if certs_location is None:
- cloud = docassemble.base.amazon.s3object(my_config)
+ cloud = docassemble.base.amazon.S3Object(my_config)
prefix = 'certs/'
else:
m = re.search(r'^s3://([^/]+)/(.*)', certs_location)
if m:
prefix = m.group(2)
my_config['bucket'] = m.group(1)
- cloud = docassemble.base.amazon.s3object(my_config)
+ cloud = docassemble.base.amazon.S3Object(my_config)
elif AZURE_ENABLED:
my_config = copy.deepcopy(azure_config)
if certs_location is None:
prefix = 'certs/'
- cloud = docassemble.base.microsoft.azureobject(my_config)
+ cloud = docassemble.base.microsoft.AzureObject(my_config)
else:
m = re.search(r'^blob://([^/]+)/([^/]+)/(.*)', certs_location)
if m:
my_config['account name'] = m.group(1)
my_config['container'] = m.group(2)
prefix = m.group(3)
- cloud = docassemble.base.microsoft.azureobject(my_config)
+ cloud = docassemble.base.microsoft.AzureObject(my_config)
if cloud is not None and prefix is not None:
success = False
if not re.search(r'/$', prefix):
diff --git a/docassemble_webapp/docassemble/webapp/interview/__init__.py b/docassemble_webapp/docassemble/webapp/interview/__init__.py
new file mode 100644
index 000000000..d027772ad
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/__init__.py
@@ -0,0 +1 @@
+from .blueprint import interview_bp # noqa: F401
diff --git a/docassemble_webapp/docassemble/webapp/interview/api.py b/docassemble_webapp/docassemble/webapp/interview/api.py
new file mode 100644
index 000000000..aec80ce30
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/api.py
@@ -0,0 +1,632 @@
+import tempfile
+import json
+import re
+import os
+from flask import request, jsonify, make_response
+from flask_cors import cross_origin
+from flask_login import current_user
+from docassemble.base.functions import safe_json
+from docassemble.base.generate_key import random_string
+from docassemble.base.interview_source import interview_source_from_string
+from docassemble.base.parse import Interview, InterviewStatus
+from docassemble.base.thread_context import this_thread
+from docassemble.webapp.admin.funcs import interview_menu
+from docassemble.webapp.api.helpers import api_verify
+from docassemble.webapp.daredis import r
+from docassemble.webapp.extensions import csrf
+from docassemble.webapp.files.file_number import get_new_file_number
+from docassemble.webapp.files.savedfile import SavedFile
+from docassemble.webapp.utils.filenames import (
+ get_ext_and_mimetype,
+ secure_filename_unicode_ok,
+ secure_filename,
+)
+from docassemble.webapp.utils.helpers import (
+ process_file,
+ illegal_variable_name,
+ from_safeid,
+ get_vars_in_use,
+ transform_json_variables,
+ parse_api_sessions_query,
+ jsonify_with_status,
+ true_or_false,
+ current_info,
+ safeid,
+)
+from docassemble.webapp.utils.hooks import url_for
+from docassemble.webapp.utils.logger import logmessage
+from .blueprint import interview_bp
+from .helpers import (
+ user_interviews,
+ get_session_variables,
+ go_back_in_session,
+ read_fields,
+ create_new_interview,
+ get_question_data,
+ set_session_variables,
+ run_action_in_session,
+)
+
+@interview_bp.route('/api/fields', methods=['POST'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['POST', 'HEAD'], automatic_options=True)
+def api_fields():
+ if not api_verify(roles=['admin', 'developer'], permissions=['template_parse']):
+ return jsonify_with_status("Access denied.", 403)
+ post_data = request.get_json(silent=True)
+ if post_data is None:
+ post_data = request.form.copy()
+ output_format = post_data.get('format', 'json')
+ if output_format not in ('json', 'yaml'):
+ return jsonify_with_status("Invalid output format.", 400)
+ if 'template' not in request.files:
+ return jsonify_with_status("File not included.", 400)
+ the_files = request.files.getlist('template')
+ if not the_files:
+ return jsonify_with_status("File not included.", 400)
+ for the_file in the_files:
+ filename = secure_filename(the_file.filename)
+ temp_file = tempfile.NamedTemporaryFile(prefix="datemp", delete=False)
+ the_file.save(temp_file.name)
+ try:
+ input_format = os.path.splitext(filename.lower())[1][1:]
+ except:
+ input_format = 'bin'
+ if input_format == 'md':
+ input_format = 'markdown'
+ if input_format not in ('docx', 'markdown', 'pdf'):
+ return jsonify_with_status("Invalid input format.", 400)
+ try:
+ output = read_fields(temp_file.name, filename, input_format, output_format)
+ except BaseException as err:
+ logmessage("api_fields: got error " + err.__class__.__name__ + ": " + str(err))
+ if output_format == 'yaml':
+ return jsonify_with_status("No fields could be found.", 400)
+ return jsonify({'fields': []})
+ break
+ if output_format == 'yaml':
+ response = make_response(output.encode('utf-8'), '200 OK')
+ response.headers['Content-type'] = 'text/plain; charset=utf-8'
+ else:
+ response = make_response(output.encode('utf-8'), 200)
+ response.headers['Content-Type'] = 'application/json; charset=utf-8'
+ return response
+
+
+@interview_bp.route('/api/session/back', methods=['POST'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['POST', 'HEAD'], automatic_options=True)
+def api_session_back():
+ if not api_verify():
+ return jsonify_with_status("Access denied.", 403)
+ post_data = request.get_json(silent=True)
+ if post_data is None:
+ post_data = request.form.copy()
+ yaml_filename = post_data.get('i', None)
+ session_id = post_data.get('session', None)
+ secret = str(post_data.get('secret', None))
+ reply_with_question = true_or_false(post_data.get('question', True))
+ if yaml_filename is None or session_id is None:
+ return jsonify_with_status("Parameters i and session are required.", 400)
+ this_thread.current_info['yaml_filename'] = yaml_filename
+ try:
+ data = go_back_in_session(yaml_filename, session_id, secret=secret, return_question=reply_with_question, use_lock=False, encode=False)
+ except BaseException as the_err:
+ return jsonify_with_status(str(the_err), 400)
+ if data is None:
+ return ('', 204)
+ if data.get('questionType', None) == 'response':
+ return data['response']
+ return jsonify(**data)
+
+
+@interview_bp.route('/api/session', methods=['GET', 'POST', 'DELETE'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['GET', 'POST', 'DELETE', 'HEAD'], automatic_options=True)
+def api_session():
+ if not api_verify():
+ return jsonify_with_status("Access denied.", 403)
+ if request.method == 'GET':
+ yaml_filename = request.args.get('i', None)
+ session_id = request.args.get('session', None)
+ secret = request.args.get('secret', None)
+ if secret is not None:
+ secret = str(secret)
+ if yaml_filename is None or session_id is None:
+ return jsonify_with_status("Parameters i and session are required.", 400)
+ this_thread.current_info['yaml_filename'] = yaml_filename
+ try:
+ variables = get_session_variables(yaml_filename, session_id, secret=secret, simplify=True, use_lock=False)
+ except BaseException as the_err:
+ return jsonify_with_status(str(the_err), 400)
+ return jsonify(variables)
+ if request.method == 'POST':
+ post_data = request.get_json(silent=True)
+ if post_data is None:
+ post_data = request.form.copy()
+ yaml_filename = post_data.get('i', None)
+ session_id = post_data.get('session', None)
+ secret = str(post_data.get('secret', None))
+ question_name = post_data.get('question_name', None)
+ treat_as_raw = true_or_false(post_data.get('raw', False))
+ advance_progress_meter = true_or_false(post_data.get('advance_progress_meter', False))
+ post_setting = not true_or_false(post_data.get('overwrite', False))
+ reply_with_question = true_or_false(post_data.get('question', True))
+ if yaml_filename is None or session_id is None:
+ return jsonify_with_status("Parameters i and session are required.", 400)
+ this_thread.current_info['yaml_filename'] = yaml_filename
+ if 'variables' in post_data and isinstance(post_data['variables'], dict):
+ variables = post_data['variables']
+ else:
+ try:
+ variables = json.loads(post_data.get('variables', '{}'))
+ except:
+ return jsonify_with_status("Malformed variables.", 400)
+ if not treat_as_raw:
+ variables = transform_json_variables(variables)
+ if 'file_variables' in post_data and isinstance(post_data['file_variables'], dict):
+ file_variables = post_data['file_variables']
+ else:
+ try:
+ file_variables = json.loads(post_data.get('file_variables', '{}'))
+ except:
+ return jsonify_with_status("Malformed list of file variables.", 400)
+ if 'delete_variables' in post_data and isinstance(post_data['delete_variables'], list):
+ del_variables = post_data['delete_variables']
+ else:
+ try:
+ del_variables = json.loads(post_data.get('delete_variables', '[]'))
+ except:
+ return jsonify_with_status("Malformed list of delete variables.", 400)
+ if 'event_list' in post_data and isinstance(post_data['event_list'], list):
+ event_list = post_data['event_list']
+ else:
+ try:
+ event_list = json.loads(post_data.get('event_list', '[]'))
+ assert isinstance(event_list, list)
+ except:
+ return jsonify_with_status("Malformed event list.", 400)
+ if not isinstance(variables, dict):
+ return jsonify_with_status("Variables data is not a dict.", 400)
+ if not isinstance(file_variables, dict):
+ return jsonify_with_status("File variables data is not a dict.", 400)
+ if not isinstance(del_variables, list):
+ return jsonify_with_status("Delete variables data is not a list.", 400)
+ if not isinstance(event_list, list):
+ return jsonify_with_status("Event list data is not a list.", 400)
+ literal_variables = {}
+ for filekey in request.files:
+ if filekey not in file_variables:
+ file_variables[filekey] = filekey
+ the_files = request.files.getlist(filekey)
+ files_to_process = []
+ if the_files:
+ for the_file in the_files:
+ safe_filename = secure_filename(the_file.filename)
+ filename = secure_filename_unicode_ok(the_file.filename)
+ file_number = get_new_file_number(session_id, safe_filename, yaml_filename)
+ extension, mimetype = get_ext_and_mimetype(filename)
+ saved_file = SavedFile(file_number, extension=extension, fix=True, should_not_exist=True)
+ temp_file = tempfile.NamedTemporaryFile(prefix="datemp", suffix='.' + extension, delete=False)
+ the_file.save(temp_file.name)
+ process_file(saved_file, temp_file.name, mimetype, extension)
+ files_to_process.append((filename, file_number, mimetype, extension))
+ file_field = file_variables[filekey]
+ if illegal_variable_name(file_field):
+ return jsonify_with_status("Malformed file variable.", 400)
+ if len(files_to_process) > 0:
+ elements = []
+ indexno = 0
+ for (filename, file_number, mimetype, extension) in files_to_process:
+ elements.append("docassemble.base.util.DAFile(" + repr(file_field + '[' + str(indexno) + ']') + ", filename=" + repr(filename) + ", number=" + str(file_number) + ", make_pngs=True, mimetype=" + repr(mimetype) + ", extension=" + repr(extension) + ")")
+ indexno += 1
+ literal_variables[file_field] = "docassemble.base.util.DAFileList(" + repr(file_field) + ", elements=[" + ", ".join(elements) + "])"
+ else:
+ literal_variables[file_field] = "None"
+ try:
+ data = set_session_variables(yaml_filename, session_id, variables, secret=secret, return_question=reply_with_question, literal_variables=literal_variables, del_variables=del_variables, question_name=question_name, event_list=event_list, advance_progress_meter=advance_progress_meter, post_setting=post_setting)
+ except BaseException as the_err:
+ return jsonify_with_status(str(the_err), 400)
+ if data is None:
+ return ('', 204)
+ if data.get('questionType', None) == 'response':
+ return data['response']
+ return jsonify(**data)
+ if request.method == 'DELETE':
+ yaml_filename = request.args.get('i', None)
+ session_id = request.args.get('session', None)
+ if yaml_filename is None or session_id is None:
+ return jsonify_with_status("Parameters i and session are required.", 400)
+ user_interviews(action='delete', filename=yaml_filename, session=session_id)
+ return ('', 204)
+ return ('', 204)
+
+
+@interview_bp.route('/api/session/new', methods=['GET'])
+@cross_origin(origins='*', methods=['GET', 'HEAD'], automatic_options=True)
+def api_session_new():
+ if not api_verify():
+ return jsonify_with_status("Access denied.", 403)
+ yaml_filename = request.args.get('i', None)
+ if yaml_filename is None:
+ return jsonify_with_status("Parameter i is required.", 400)
+ secret = request.args.get('secret', None)
+ if secret is None:
+ new_secret = True
+ secret = random_string(16)
+ else:
+ new_secret = False
+ secret = str(secret)
+ url_args = {}
+ for argname in request.args:
+ if argname in ('i', 'secret', 'key'):
+ continue
+ if re.match('[A-Za-z_][A-Za-z0-9_]*', argname):
+ url_args[argname] = request.args[argname]
+ this_thread.current_info['yaml_filename'] = yaml_filename
+ try:
+ (encrypted, session_id) = create_new_interview(yaml_filename, secret, url_args, None, request)
+ except BaseException as err:
+ return jsonify_with_status(err.__class__.__name__ + ': ' + str(err), 400)
+ if encrypted and new_secret:
+ return jsonify({'session': session_id, 'i': yaml_filename, 'secret': secret, 'encrypted': encrypted})
+ return jsonify({'session': session_id, 'i': yaml_filename, 'encrypted': encrypted})
+
+
+@interview_bp.route('/api/session/question', methods=['GET'])
+@cross_origin(origins='*', methods=['GET', 'HEAD'], automatic_options=True)
+def api_session_question():
+ if not api_verify():
+ return jsonify_with_status("Access denied.", 403)
+ yaml_filename = request.args.get('i', None)
+ session_id = request.args.get('session', None)
+ secret = request.args.get('secret', None)
+ if secret is not None:
+ secret = str(secret)
+ if yaml_filename is None or session_id is None:
+ return jsonify_with_status("Parameters i and session are required.", 400)
+ this_thread.current_info['yaml_filename'] = yaml_filename
+ try:
+ data = get_question_data(yaml_filename, session_id, secret)
+ except BaseException as err:
+ return jsonify_with_status(str(err), 400)
+ if data.get('questionType', None) == 'response':
+ return data['response']
+ return jsonify(**data)
+
+
+@interview_bp.route('/api/session/action', methods=['POST'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['POST', 'HEAD'], automatic_options=True)
+def api_session_action():
+ if not api_verify():
+ return jsonify_with_status("Access denied.", 403)
+ post_data = request.get_json(silent=True)
+ if post_data is None:
+ post_data = request.form.copy()
+ result = run_action_in_session(post_data)
+ if not isinstance(result, dict):
+ return result
+ if result['status'] == 'success':
+ return ('', 204)
+ return jsonify_with_status(result['message'], 400)
+
+
+@interview_bp.route('/api/list', methods=['GET'])
+@cross_origin(origins='*', methods=['GET', 'HEAD'], automatic_options=True)
+def api_list():
+ if not api_verify():
+ return jsonify_with_status("Access denied.", 403)
+ return jsonify(interview_menu(absolute_urls=true_or_false(request.args.get('absolute_urls', True)), start_new=False, tag=request.args.get('tag', None)))
+
+
+@interview_bp.route('/api/user/interviews', methods=['GET', 'DELETE'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['GET', 'DELETE', 'HEAD'], automatic_options=True)
+def api_user_interviews():
+ if not api_verify():
+ return jsonify_with_status("Access denied.", 403)
+ filename = request.args.get('i', None)
+ session_id = request.args.get('session', None)
+ query = request.args.get('query', None)
+ try:
+ query = parse_api_sessions_query(query)
+ except:
+ return jsonify_with_status("Invalid query parameter", 400)
+ tag = request.args.get('tag', None)
+ secret = request.args.get('secret', None)
+ if secret is not None:
+ secret = str(secret)
+ include_dict = true_or_false(request.args.get('include_dictionary', False))
+ next_id_code = request.args.get('next_id', None)
+ if next_id_code:
+ try:
+ start_id = int(from_safeid(next_id_code))
+ assert start_id >= 0
+ except:
+ start_id = None
+ else:
+ start_id = None
+ if request.method == 'GET':
+ try:
+ (the_list, start_id) = user_interviews(user_id=current_user.id, secret=secret, filename=filename, session=session_id, query=query, exclude_invalid=False, tag=tag, include_dict=include_dict, start_id=start_id)
+ except:
+ return jsonify_with_status("Error reading interview list.", 400)
+ if start_id is None:
+ next_id = None
+ else:
+ next_id = safeid(str(start_id))
+ return jsonify({'next_id': next_id, 'items': safe_json(the_list)})
+ if request.method == 'DELETE':
+ start_id = None
+ while True:
+ try:
+ (the_list, start_id) = user_interviews(user_id=current_user.id, filename=filename, session=session_id, query=query, exclude_invalid=False, tag=tag, include_dict=False, start_id=start_id)
+ except:
+ return jsonify_with_status("Error reading interview list.", 400)
+ for info in the_list:
+ user_interviews(user_id=info['user_id'], action='delete', filename=info['filename'], session=info['session'])
+ if start_id is None:
+ break
+ return ('', 204)
+ return ('', 204)
+
+
+@interview_bp.route('/api/interviews', methods=['GET', 'DELETE'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['GET', 'DELETE', 'HEAD'], automatic_options=True)
+def api_interviews():
+ if not api_verify(roles=['admin', 'advocate'], permissions=['access_sessions']):
+ return jsonify_with_status("Access denied.", 403)
+ filename = request.args.get('i', None)
+ session_id = request.args.get('session', None)
+ query = request.args.get('query', None)
+ try:
+ query = parse_api_sessions_query(query)
+ except:
+ return jsonify_with_status("Invalid query parameter", 400)
+ tag = request.args.get('tag', None)
+ secret = request.args.get('secret', None)
+ if secret is not None:
+ secret = str(secret)
+ include_dict = true_or_false(request.args.get('include_dictionary', False))
+ next_id_code = request.args.get('next_id', None)
+ if next_id_code:
+ try:
+ start_id = int(from_safeid(next_id_code))
+ assert start_id >= 0
+ except:
+ start_id = None
+ else:
+ start_id = None
+ if request.method == 'GET':
+ try:
+ (the_list, start_id) = user_interviews(secret=secret, filename=filename, session=session_id, query=query, exclude_invalid=False, tag=tag, include_dict=include_dict, start_id=start_id)
+ except BaseException as err:
+ return jsonify_with_status("Error reading interview list: " + str(err), 400)
+ if start_id is None:
+ next_id = None
+ else:
+ next_id = safeid(str(start_id))
+ return jsonify({'next_id': next_id, 'items': safe_json(the_list)})
+ if request.method == 'DELETE':
+ if not current_user.has_role_or_permission('admin', 'advocate', permissions=['edit_sessions']):
+ return jsonify_with_status("Access denied.", 403)
+ start_id = None
+ while True:
+ try:
+ (the_list, start_id) = user_interviews(filename=filename, session=session_id, query=query, exclude_invalid=False, tag=tag, include_dict=False, start_id=start_id)
+ except:
+ return jsonify_with_status("Error reading interview list.", 400)
+ for info in the_list:
+ if info['user_id'] is not None:
+ user_interviews(user_id=info['user_id'], action='delete', filename=info['filename'], session=info['session'])
+ else:
+ user_interviews(temp_user_id=info['temp_user_id'], action='delete', filename=info['filename'], session=info['session'])
+ if start_id is None:
+ break
+ return ('', 204)
+ return ('', 204)
+
+
+@interview_bp.route('/api/users/interviews', methods=['GET', 'DELETE'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['GET', 'DELETE', 'HEAD'], automatic_options=True)
+def api_users_interviews():
+ if not api_verify(roles=['admin', 'advocate'], permissions=['access_sessions']):
+ return jsonify_with_status("Access denied.", 403)
+ user_id = request.args.get('user_id', None)
+ filename = request.args.get('i', None)
+ session_id = request.args.get('session', None)
+ query = request.args.get('query', None)
+ try:
+ query = parse_api_sessions_query(query)
+ except:
+ return jsonify_with_status("Invalid query parameter", 400)
+ secret = request.args.get('secret', None)
+ tag = request.args.get('tag', None)
+ next_id_code = request.args.get('next_id', None)
+ if next_id_code:
+ try:
+ start_id = int(from_safeid(next_id_code))
+ assert start_id >= 0
+ except:
+ start_id = None
+ else:
+ start_id = None
+ if secret is not None:
+ secret = str(secret)
+ if request.method == 'GET':
+ include_dict = true_or_false(request.args.get('include_dictionary', False))
+ try:
+ (the_list, start_id) = user_interviews(user_id=user_id, secret=secret, exclude_invalid=False, tag=tag, filename=filename, session=session_id, query=query, include_dict=include_dict, start_id=start_id)
+ except BaseException as err:
+ return jsonify_with_status("Error getting interview list. " + str(err), 400)
+ if start_id is None:
+ next_id = None
+ else:
+ next_id = safeid(str(start_id))
+ return jsonify({'next_id': next_id, 'items': safe_json(the_list)})
+ if request.method == 'DELETE':
+ start_id = None
+ while True:
+ try:
+ (the_list, start_id) = user_interviews(user_id=user_id, exclude_invalid=False, tag=tag, filename=filename, session=session_id, query=query, include_dict=False, start_id=start_id)
+ except:
+ return jsonify_with_status("Error reading interview list.", 400)
+ for info in the_list:
+ user_interviews(user_id=info['user_id'], action='delete', filename=info['filename'], session=info['session'])
+ if start_id is None:
+ break
+ return ('', 204)
+ return ('', 204)
+
+
+@interview_bp.route('/api/user//interviews', methods=['GET', 'DELETE'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['GET', 'DELETE', 'HEAD'], automatic_options=True)
+def api_user_user_id_interviews(user_id):
+ if not api_verify():
+ return jsonify_with_status("Access denied.", 403)
+ if not (current_user.id == user_id or current_user.has_role_or_permission('admin', 'advocate', permissions=['access_sessions'])):
+ return jsonify_with_status("Access denied.", 403)
+ filename = request.args.get('i', None)
+ session_id = request.args.get('session', None)
+ query = request.args.get('query', None)
+ try:
+ query = parse_api_sessions_query(query)
+ except:
+ return jsonify_with_status("Invalid query parameter", 400)
+ secret = request.args.get('secret', None)
+ tag = request.args.get('tag', None)
+ next_id_code = request.args.get('next_id', None)
+ if next_id_code:
+ try:
+ start_id = int(from_safeid(next_id_code))
+ assert start_id >= 0
+ except:
+ start_id = None
+ else:
+ start_id = None
+ if secret is not None:
+ secret = str(secret)
+ include_dict = true_or_false(request.args.get('include_dictionary', False))
+ if request.method == 'GET':
+ try:
+ (the_list, start_id) = user_interviews(user_id=user_id, secret=secret, exclude_invalid=False, tag=tag, filename=filename, session=session_id, query=query, include_dict=include_dict, start_id=start_id)
+ except:
+ return jsonify_with_status("Error reading interview list.", 400)
+ if start_id is None:
+ next_id = None
+ else:
+ next_id = safeid(str(start_id))
+ return jsonify({'next_id': next_id, 'items': safe_json(the_list)})
+ if request.method == 'DELETE':
+ start_id = None
+ while True:
+ try:
+ (the_list, start_id) = user_interviews(user_id=user_id, exclude_invalid=False, tag=tag, filename=filename, session=session_id, query=query, include_dict=False, start_id=start_id)
+ except:
+ return jsonify_with_status("Error reading interview list.", 400)
+ for info in the_list:
+ user_interviews(user_id=info['user_id'], action='delete', filename=info['filename'], session=info['session'])
+ if start_id is None:
+ break
+ return ('', 204)
+ return ('', 204)
+
+
+@interview_bp.route('/api/resume_url', methods=['POST'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['POST', 'HEAD'], automatic_options=True)
+def api_resume_url():
+ if not api_verify():
+ return jsonify_with_status("Access denied.", 403)
+ post_data = request.get_json(silent=True)
+ if post_data is None:
+ post_data = request.form.copy()
+ filename = post_data.get('i', None)
+ if filename is None:
+ return jsonify_with_status("No filename supplied.", 400)
+ session_id = post_data.get('session', post_data.get('session_id', None))
+ if 'url_args' in post_data:
+ if isinstance(post_data['url_args'], dict):
+ url_args = post_data['url_args']
+ else:
+ try:
+ url_args = json.loads(post_data['url_args'])
+ assert isinstance(url_args, dict)
+ except:
+ return jsonify_with_status("Malformed URL arguments", 400)
+ else:
+ url_args = {}
+ try:
+ one_time = bool(int(post_data.get('one_time', 0)))
+ except:
+ one_time = False
+ try:
+ expire = int(post_data.get('expire', 3600))
+ assert expire > 0
+ except:
+ return jsonify_with_status("Invalid number of seconds.", 400)
+ info = {'i': filename}
+ if session_id:
+ info['session'] = session_id
+ if one_time:
+ info['once'] = True
+ if len(url_args):
+ info['url_args'] = url_args
+ while True:
+ code = random_string(32)
+ the_key = 'da:resume_interview:' + code
+ if r.get(the_key) is None:
+ break
+ pipe = r.pipeline()
+ pipe.set(the_key, json.dumps(info))
+ pipe.expire(the_key, expire)
+ pipe.execute()
+ return jsonify(url_for('interview.launch', c=code, _external=True))
+
+
+@interview_bp.route('/api/clear_cache', methods=['POST'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['POST', 'HEAD'], automatic_options=True)
+def api_clear_cache():
+ if not api_verify(roles=['admin', 'developer'], permissions=['playground_control']):
+ return jsonify_with_status("Access denied.", 403)
+ for key in r.keys('da:interviewsource:*'):
+ r.incr(key.decode())
+ return ('', 204)
+
+
+@interview_bp.route('/api/interview_data', methods=['GET'])
+@csrf.exempt
+@cross_origin(origins='*', methods=['GET', 'HEAD'], automatic_options=True)
+def api_interview_data():
+ if not api_verify(roles=['admin', 'developer'], permissions=['interview_data']):
+ return jsonify_with_status("Access denied.", 403)
+ filename = request.args.get('i', None)
+ if filename is None:
+ return jsonify_with_status("No filename supplied.", 400)
+ try:
+ interview_source = interview_source_from_string(filename, testing=True)
+ except BaseException as err:
+ return jsonify_with_status("Error finding interview: " + str(err), 400)
+ try:
+ interview = Interview(source=interview_source)
+ except BaseException as err:
+ return jsonify_with_status("Error finding interview: " + str(err), 400)
+ device_id = this_thread.current_info['user']['device_id']
+ interview_status = InterviewStatus(current_info=current_info(yaml=filename, req=request, action=None, device_id=device_id))
+ m = re.search('docassemble.playground([0-9]+)([^:]*):', filename)
+ if m:
+ use_playground = bool(current_user.id == int(m.group(1)))
+ if m.group(2) != '':
+ current_project = m.group(2)
+ else:
+ current_project = 'default'
+ else:
+ use_playground = False
+ current_project = 'default'
+ variables_json, vocab_list, vocab_dict, ac_list = get_vars_in_use(interview, interview_status, debug_mode=False, return_json=True, use_playground=use_playground, current_project=current_project) # pylint: disable=unused-variable
+ return jsonify({'names': variables_json, 'vocabulary': list(vocab_list)})
diff --git a/docassemble_webapp/docassemble/webapp/interview/blueprint.py b/docassemble_webapp/docassemble/webapp/interview/blueprint.py
new file mode 100644
index 000000000..0dec3b67d
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/blueprint.py
@@ -0,0 +1,6 @@
+from flask import Blueprint
+
+interview_bp = Blueprint(
+ 'interview',
+ __name__
+)
diff --git a/docassemble_webapp/docassemble/webapp/interview/common.py b/docassemble_webapp/docassemble/webapp/interview/common.py
new file mode 100644
index 000000000..fd524aca5
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/common.py
@@ -0,0 +1,22 @@
+import datetime
+from sqlalchemy import select
+from docassemble.base.generate_key import random_alphanumeric
+from docassemble.webapp.extensions import db
+from docassemble.webapp.lock import obtain_lock, release_lock
+from docassemble.webapp.utils.encryption import encrypt_dictionary
+from docassemble.webapp.interview.dictionary import fresh_dictionary
+from .models import UserDict
+
+def get_unique_name(filename, secret):
+ nowtime = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
+ while True:
+ newname = random_alphanumeric(32)
+ obtain_lock(newname, filename)
+ existing_key = db.session.execute(select(UserDict).filter_by(key=newname)).first()
+ if existing_key:
+ release_lock(newname, filename)
+ continue
+ new_user_dict = UserDict(modtime=nowtime, key=newname, filename=filename, dictionary=encrypt_dictionary(fresh_dictionary(), secret))
+ db.session.add(new_user_dict)
+ db.session.commit()
+ return newname
diff --git a/docassemble_webapp/docassemble/webapp/interview/config.py b/docassemble_webapp/docassemble/webapp/interview/config.py
new file mode 100644
index 000000000..37c422dcb
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/config.py
@@ -0,0 +1,87 @@
+from markupsafe import Markup
+from docassemble.base.parse import set_initial_dict
+from docassemble.base.functions import DANav
+from docassemble.webapp.config import daconfig, COOKIELESS_SESSIONS, DEFAULT_LANGUAGE
+
+initial_dict = {'_internal': {'session_local': {}, 'device_local': {}, 'user_local': {}, 'dirty': {}, 'progress': 0, 'tracker': 0, 'docvar': {}, 'doc_cache': {}, 'steps': 1, 'steps_offset': 0, 'secret': None, 'informed': {}, 'livehelp': {'availability': 'unavailable', 'mode': 'help', 'roles': [], 'partner_roles': []}, 'answered': set(), 'answers': {}, 'objselections': {}, 'starttime': None, 'modtime': None, 'accesstime': {}, 'tasks': {}, 'gather': [], 'event_stack': {}, 'misc': {}}, 'url_args': {}, 'nav': DANav()}
+
+if 'initial dict' in daconfig:
+ initial_dict.update(daconfig['initial dict'])
+
+set_initial_dict(initial_dict)
+
+if COOKIELESS_SESSIONS:
+ INDEX_PATH = '/i'
+ HTML_INDEX_PATH = '/interview'
+else:
+ INDEX_PATH = '/interview'
+ HTML_INDEX_PATH = '/i'
+
+def get_page_parts():
+ the_page_parts = {}
+ if 'global footer' in daconfig:
+ if isinstance(daconfig['global footer'], dict):
+ the_page_parts['global footer'] = {}
+ for lang, val in daconfig['global footer'].items():
+ the_page_parts['global footer'][lang] = Markup(val)
+ else:
+ the_page_parts['global footer'] = {'*': Markup(str(daconfig['global footer']))}
+
+ for page_key in ('login page', 'register page', 'interview page', 'start page', 'profile page', 'reset password page', 'forgot password page', 'change password page', '404 page', 'error page'):
+ for part_key in ('title', 'tab title', 'extra css', 'extra javascript', 'heading', 'pre', 'submit', 'post', 'footer', 'navigation bar html'):
+ key = page_key + ' ' + part_key
+ if key in daconfig:
+ if isinstance(daconfig[key], dict):
+ the_page_parts[key] = {}
+ for lang, val in daconfig[key].items():
+ the_page_parts[key][lang] = Markup(val)
+ else:
+ the_page_parts[key] = {'*': Markup(str(daconfig[key]))}
+
+ the_main_page_parts = {}
+ lang_list = set()
+ main_page_parts_list = (
+ 'main page back button label',
+ 'main page continue button label',
+ 'main page corner back button label',
+ 'main page exit label',
+ 'main page exit link',
+ 'main page exit url',
+ 'main page footer',
+ 'main page help label',
+ 'main page logo',
+ 'main page navigation bar html',
+ 'main page post',
+ 'main page pre',
+ 'main page resume button label',
+ 'main page right',
+ 'main page short logo',
+ 'main page short title',
+ 'main page submit',
+ 'main page subtitle',
+ 'main page title url opens in other window',
+ 'main page title url',
+ 'main page title',
+ 'main page under')
+ for key in main_page_parts_list:
+ if key in daconfig and isinstance(daconfig[key], dict):
+ for lang in daconfig[key]:
+ lang_list.add(lang)
+ lang_list.add(DEFAULT_LANGUAGE)
+ lang_list.add('*')
+ for lang in lang_list:
+ the_main_page_parts[lang] = {}
+ for key in main_page_parts_list:
+ for lang in lang_list:
+ if key in daconfig:
+ if isinstance(daconfig[key], dict):
+ the_main_page_parts[lang][key] = daconfig[key].get(lang, daconfig[key].get('*', ''))
+ else:
+ the_main_page_parts[lang][key] = daconfig[key]
+ else:
+ the_main_page_parts[lang][key] = ''
+ if the_main_page_parts[DEFAULT_LANGUAGE][key] == '' and the_main_page_parts['*'][key] != '':
+ the_main_page_parts[DEFAULT_LANGUAGE][key] = the_main_page_parts['*'][key]
+ return (the_page_parts, the_main_page_parts)
+
+(page_parts, main_page_parts) = get_page_parts()
diff --git a/docassemble_webapp/docassemble/webapp/interview/dictionary.py b/docassemble_webapp/docassemble/webapp/interview/dictionary.py
new file mode 100644
index 000000000..3f47f5faa
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/dictionary.py
@@ -0,0 +1,23 @@
+import copy
+import datetime
+from flask_login import current_user
+from docassemble.webapp.interview.config import initial_dict
+
+def fresh_dictionary():
+ the_dict = copy.deepcopy(initial_dict)
+ add_timestamps(the_dict)
+ return the_dict
+
+
+def add_timestamps(the_dict, manual_user_id=None):
+ nowtime = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
+ the_dict['_internal']['starttime'] = nowtime
+ the_dict['_internal']['modtime'] = nowtime
+ if manual_user_id is not None or (current_user and current_user.is_authenticated):
+ if manual_user_id is not None:
+ the_user_id = manual_user_id
+ else:
+ the_user_id = current_user.id
+ the_dict['_internal']['accesstime'][the_user_id] = nowtime
+ else:
+ the_dict['_internal']['accesstime'][-1] = nowtime
diff --git a/docassemble_webapp/docassemble/webapp/interview/helpers.py b/docassemble_webapp/docassemble/webapp/interview/helpers.py
new file mode 100644
index 000000000..a57eed8c7
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/helpers.py
@@ -0,0 +1,1705 @@
+import zoneinfo
+import copy
+import datetime
+import json
+import re
+import traceback
+import os
+from pygments import highlight
+from pygments.lexers import YamlLexer # pylint: disable=no-name-in-module
+from pygments.formatters.html import HtmlFormatter
+from flask import request, make_response, session, current_app
+from flask_login import current_user
+from flask_wtf.csrf import generate_csrf
+from sqlalchemy import or_, update, delete, select, not_, and_
+from docassemble.base import DA
+from docassemble.base.error import DAErrorMissingVariable, DAException
+from docassemble.base.functions import get_message_log, serializable_dict, dict_as_json
+from docassemble.base.generate_key import random_alphanumeric, random_string
+from docassemble.base.hooks import manage_chat_logs, manage_global_objects, manage_email_server_objects, manage_tts_objects
+from docassemble.base.interview_cache import get_interview
+from docassemble.base.language.control import get_language
+from docassemble.base.language.words import word
+from docassemble.base.pandoc import word_to_markdown
+from docassemble.base.parse import InterviewStatus
+from docassemble.base.pdftk import read_fields as base_read_fields
+from docassemble.base.save_status import SS_NEW, SS_OVERWRITE, SS_IGNORE
+from docassemble.base.thread_context import global_context, this_thread, copy_of_globals
+from docassemble.webapp.config import (
+ PAGINATION_LIMIT_PLUS_ONE,
+ NOTIFICATION_CONTAINER,
+ NOTIFICATION_MESSAGE,
+ default_short_title,
+ DEFAULT_LANGUAGE,
+ ALLOW_REGISTRATION,
+ SHOW_LOGIN,
+ default_title,
+ ROOT,
+ REQUIRE_IDEMPOTENT,
+ PAGINATION_LIMIT,
+ da_version,
+ daconfig,
+)
+from docassemble.webapp.extensions import db
+from docassemble.webapp.files.savedfile import SavedFile
+from docassemble.webapp.hooks.impl import hookimpl
+from docassemble.webapp.lock import lock_context, obtain_lock, release_lock
+from docassemble.webapp.main.hooks import get_default_timezone
+from docassemble.webapp.main.models import Uploads, UploadsUserAuth
+from docassemble.webapp.packages.models import PackageAuth
+from docassemble.webapp.sessions import update_session, get_session, session_context
+from docassemble.webapp.users.common import get_person
+from docassemble.webapp.users.models import UserModel, UserRoles, UserAuthModel
+from docassemble.webapp.interview.dictionary import fresh_dictionary
+from docassemble.webapp.utils.constants import NoneType
+from docassemble.webapp.utils.encryption import (
+ pack_dictionary,
+ decrypt_dictionary,
+ encrypt_dictionary,
+ unpack_dictionary,
+ nice_date_from_utc,
+)
+from docassemble.webapp.utils.helpers import (
+ title_converter,
+ docx_variable_fix,
+ current_info,
+ exit_href,
+ illegal_variable_name,
+ transform_json_variables,
+ reset_session,
+ add_referer,
+ contains_volatile,
+ jsonify_with_status,
+ MD5Hash,
+ pad_to_16,
+ sanitize,
+ true_or_false,
+ manual_checkout,
+ custom_send_file,
+ CAN_CONVERT_WORD,
+)
+from docassemble.webapp.utils.hooks import url_for
+from docassemble.webapp.utils.logger import logmessage
+from .config import main_page_parts, page_parts
+from .models import UserDict, UserDictKeys
+from .user_dict import fetch_user_dict, fetch_previous_user_dict
+
+def sub_temp_user_dict_key(temp_user_id, user_id):
+ temp_interviews = []
+ for record in db.session.execute(select(UserDictKeys).filter_by(temp_user_id=temp_user_id).with_for_update()).scalars():
+ record.temp_user_id = None
+ record.user_id = user_id
+ temp_interviews.append((record.filename, record.key))
+ db.session.commit()
+ return temp_interviews
+
+
+def sub_temp_other(user):
+ if 'tempuser' in session:
+ device_id = request.cookies.get('ds', None)
+ if device_id is None:
+ device_id = random_string(16)
+ url_root = daconfig.get('url root', 'http://localhost') + daconfig.get('root', '/')
+ url = url_root + 'interview'
+ role_list = [role.name for role in user.roles]
+ if len(role_list) == 0:
+ role_list = ['user']
+ the_current_info = {'user': {'email': user.email, 'roles': role_list, 'the_user_id': user.id, 'theid': user.id, 'firstname': user.first_name, 'lastname': user.last_name, 'nickname': user.nickname, 'country': user.country, 'subdivisionfirst': user.subdivisionfirst, 'subdivisionsecond': user.subdivisionsecond, 'subdivisionthird': user.subdivisionthird, 'organization': user.organization, 'timezone': user.timezone, 'language': user.language, 'location': None, 'session_uid': 'admin', 'device_id': device_id}, 'session': None, 'secret': None, 'yaml_filename': None, 'url': url, 'url_root': url_root, 'encrypted': False, 'action': None, 'interface': 'web', 'arguments': {}}
+ this_thread.current_info = the_current_info
+ manage_chat_logs(0, temp_user_id=session['tempuser'], new_user_id=user.id)
+ manage_global_objects(0, temp_user_id=session['tempuser'], new_user_id=user.id, oldsecret=str(request.cookies.get('secret', None)), newsecret=session.get('newsecret', None))
+ db.session.execute(update(UploadsUserAuth).where(UploadsUserAuth.temp_user_id == int(session['tempuser'])).values(user_id=user.id, temp_user_id=None))
+ db.session.commit()
+ del session['tempuser']
+
+
+def save_user_dict_key(session_id, filename, priors=False, user=None):
+ if user is not None:
+ user_id = user.id
+ is_auth = True
+ else:
+ if current_user.is_authenticated:
+ is_auth = True
+ user_id = current_user.id
+ else:
+ is_auth = False
+ user_id = session.get('tempuser', None)
+ if user_id is None:
+ logmessage("save_user_dict_key: no user ID available for saving")
+ return
+ # logmessage("save_user_dict_key: called")
+ the_interview_list = set([filename])
+ found = set()
+ if priors:
+ for the_record in db.session.execute(select(UserDict.filename).filter_by(key=session_id).group_by(UserDict.filename)):
+ the_interview_list.add(the_record.filename)
+ for filename_to_search in the_interview_list:
+ if is_auth:
+ for the_record in db.session.execute(select(UserDictKeys).filter_by(key=session_id, filename=filename_to_search, user_id=user_id)):
+ found.add(filename_to_search)
+ else:
+ for the_record in db.session.execute(select(UserDictKeys).filter_by(key=session_id, filename=filename_to_search, temp_user_id=user_id)):
+ found.add(filename_to_search)
+ for filename_to_save in (the_interview_list - found):
+ if is_auth:
+ new_record = UserDictKeys(key=session_id, filename=filename_to_save, user_id=user_id)
+ else:
+ new_record = UserDictKeys(key=session_id, filename=filename_to_save, temp_user_id=user_id)
+ db.session.add(new_record)
+ db.session.commit()
+
+
+def save_user_dict(user_code, user_dict, filename, secret=None, changed=False, encrypt=True, manual_user_id=None, steps=None, max_indexno=None):
+ # logmessage("save_user_dict: called with encrypt " + str(encrypt))
+ if REQUIRE_IDEMPOTENT:
+ for var_name in ('x', 'i', 'j', 'k', 'l', 'm', 'n', '__DANEWOBJECT'):
+ if var_name in user_dict:
+ del user_dict[var_name]
+ user_dict['_internal']['objselections'] = {}
+ if 'session_local' in user_dict:
+ del user_dict['session_local']
+ if 'device_local' in user_dict:
+ del user_dict['device_local']
+ if 'user_local' in user_dict:
+ del user_dict['user_local']
+ nowtime = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
+ if steps is not None:
+ user_dict['_internal']['steps'] = steps
+ user_dict['_internal']['modtime'] = nowtime
+ if manual_user_id is not None or (current_user and current_user.is_authenticated):
+ if manual_user_id is not None:
+ the_user_id = manual_user_id
+ else:
+ the_user_id = current_user.id
+ user_dict['_internal']['accesstime'][the_user_id] = nowtime
+ else:
+ user_dict['_internal']['accesstime'][-1] = nowtime
+ the_user_id = None
+ if changed is True:
+ if encrypt:
+ new_record = UserDict(modtime=nowtime, key=user_code, dictionary=encrypt_dictionary(user_dict, secret), filename=filename, user_id=the_user_id, encrypted=True)
+ else:
+ new_record = UserDict(modtime=nowtime, key=user_code, dictionary=pack_dictionary(user_dict), filename=filename, user_id=the_user_id, encrypted=False)
+ db.session.add(new_record)
+ db.session.commit()
+ else:
+ if max_indexno is None:
+ max_indexno = db.session.execute(select(db.func.max(UserDict.indexno)).where(and_(UserDict.key == user_code, UserDict.filename == filename))).scalar()
+ if max_indexno is None:
+ if encrypt:
+ new_record = UserDict(modtime=nowtime, key=user_code, dictionary=encrypt_dictionary(user_dict, secret), filename=filename, user_id=the_user_id, encrypted=True)
+ else:
+ new_record = UserDict(modtime=nowtime, key=user_code, dictionary=pack_dictionary(user_dict), filename=filename, user_id=the_user_id, encrypted=False)
+ db.session.add(new_record)
+ db.session.commit()
+ else:
+ for record in db.session.execute(select(UserDict).filter_by(key=user_code, filename=filename, indexno=max_indexno).with_for_update()).scalars():
+ if encrypt:
+ record.dictionary = encrypt_dictionary(user_dict, secret)
+ record.modtime = nowtime
+ record.encrypted = True
+ else:
+ record.dictionary = pack_dictionary(user_dict)
+ record.modtime = nowtime
+ record.encrypted = False
+ db.session.commit()
+
+
+def get_existing_session(yaml_filename, secret):
+ keys = [result.key for result in db.session.execute(select(UserDictKeys.filename, UserDictKeys.key).where(and_(UserDictKeys.user_id == current_user.id, UserDictKeys.filename == yaml_filename)).order_by(UserDictKeys.indexno))]
+ for key in keys:
+ try:
+ steps, user_dict, is_encrypted = fetch_user_dict(key, yaml_filename, secret=secret) # pylint: disable=unused-variable
+ except:
+ logmessage("get_existing_session: unable to decrypt existing interview session " + key)
+ continue
+ update_session(yaml_filename, uid=key, key_logged=True, encrypted=is_encrypted)
+ return key, is_encrypted
+ return None, True
+
+
+def advance_progress(user_dict, interview):
+ if user_dict['_internal']['progress'] is None:
+ return
+ if hasattr(interview, 'progress_bar_multiplier'):
+ multiplier = interview.progress_bar_multiplier
+ else:
+ multiplier = 0.05
+ if hasattr(interview, 'progress_bar_method') and interview.progress_bar_method == 'stepped':
+ next_part = 100.0
+ for value in sorted(interview.progress_points):
+ if value > user_dict['_internal']['progress']:
+ next_part = value
+ break
+ user_dict['_internal']['progress'] += multiplier*(next_part-user_dict['_internal']['progress'])
+ else:
+ user_dict['_internal']['progress'] += multiplier*(100-user_dict['_internal']['progress'])
+
+
+def delete_temp_user_data(temp_user_id, r):
+ db.session.execute(delete(UserDictKeys).where(UserDictKeys.temp_user_id == temp_user_id))
+ db.session.commit()
+ db.session.execute(delete(UploadsUserAuth).where(UploadsUserAuth.temp_user_id == temp_user_id))
+ db.session.commit()
+ manage_chat_logs(1, temp_user_id=temp_user_id)
+ manage_global_objects(1, temp_user_id=temp_user_id)
+ manage_email_server_objects(0, temp_user_id=temp_user_id)
+ keys_to_delete = set()
+ for key in r.keys('*userid:t' + str(temp_user_id)):
+ keys_to_delete.add(key)
+ for key in r.keys('*userid:t' + str(temp_user_id) + ':*'):
+ keys_to_delete.add(key)
+ for key in keys_to_delete:
+ r.delete(key)
+
+
+def delete_user_data(user_id, r, r_user):
+ db.session.execute(delete(UserDict).where(UserDict.user_id == user_id))
+ db.session.commit()
+ db.session.execute(delete(UserDictKeys).where(UserDictKeys.user_id == user_id))
+ db.session.commit()
+ db.session.execute(delete(UploadsUserAuth).where(UploadsUserAuth.user_id == user_id))
+ db.session.commit()
+ manage_chat_logs(2, user_id=user_id)
+ manage_global_objects(2, user_id=user_id)
+ for package_auth in db.session.execute(select(PackageAuth).filter_by(user_id=user_id)).scalars():
+ package_auth.user_id = 1
+ db.session.commit()
+ manage_email_server_objects(1, user_id=user_id)
+ db.session.execute(delete(UserRoles).where(UserRoles.user_id == user_id))
+ db.session.commit()
+ for user_auth in db.session.execute(select(UserAuthModel).filter_by(user_id=user_id).with_for_update()).scalars():
+ user_auth.password = ''
+ user_auth.reset_password_token = ''
+ db.session.commit()
+ for section in ('playground', 'playgroundmodules', 'playgroundpackages', 'playgroundsources', 'playgroundstatic', 'playgroundtemplate'):
+ the_section = SavedFile(user_id, section=section)
+ the_section.delete()
+ old_email = None
+ for user_object in db.session.execute(select(UserModel).filter_by(id=user_id)).scalars():
+ old_email = user_object.email
+ user_object.active = False
+ user_object.first_name = ''
+ user_object.last_name = ''
+ user_object.nickname = ''
+ user_object.email = None
+ user_object.country = ''
+ user_object.subdivisionfirst = ''
+ user_object.subdivisionsecond = ''
+ user_object.subdivisionthird = ''
+ user_object.organization = ''
+ user_object.timezone = None
+ user_object.language = None
+ user_object.pypi_username = None
+ user_object.pypi_password = None
+ user_object.otp_secret = None
+ user_object.confirmed_at = None
+ user_object.last_login = None
+ user_object.social_id = 'disabled$' + str(user_id)
+ db.session.commit()
+ keys_to_delete = set()
+ for key in r.keys('*userid:' + str(user_id)):
+ keys_to_delete.add(key)
+ for key in r.keys('*userid:' + str(user_id) + ':*'):
+ keys_to_delete.add(key)
+ for key in keys_to_delete:
+ r.delete(key)
+ keys_to_delete = set()
+ for key in r_user.keys('*:user:' + str(old_email)):
+ keys_to_delete.add(key)
+ for key in keys_to_delete:
+ r_user.delete(key)
+
+
+# @elapsed('reset_user_dict')
+def reset_user_dict(user_code, filename, user_id=None, temp_user_id=None, force=False):
+ # logmessage("reset_user_dict called with " + str(user_code) + " and " + str(filename) + " and " + str(user_id) + " and " + str(temp_user_id) + " and " + str(force))
+ user_type = ''
+ if force:
+ the_user_id = None
+ else:
+ if user_id is None and temp_user_id is None:
+ if current_user.is_authenticated:
+ user_type = 'user'
+ the_user_id = current_user.id
+ else:
+ user_type = 'tempuser'
+ the_user_id = session.get('tempuser', None)
+ elif user_id is not None:
+ user_type = 'user'
+ the_user_id = user_id
+ else:
+ user_type = 'tempuser'
+ the_user_id = temp_user_id
+ if the_user_id is None:
+ db.session.execute(delete(UserDictKeys).filter_by(key=user_code, filename=filename))
+ db.session.commit()
+ do_delete = True
+ else:
+ if user_type == 'user':
+ db.session.execute(delete(UserDictKeys).filter_by(key=user_code, filename=filename, user_id=the_user_id))
+ else:
+ db.session.execute(delete(UserDictKeys).filter_by(key=user_code, filename=filename, temp_user_id=the_user_id))
+ db.session.commit()
+ existing_user_dict_key = db.session.execute(select(UserDictKeys).filter_by(key=user_code, filename=filename)).scalar()
+ do_delete = not bool(existing_user_dict_key)
+ if not force:
+ files_to_save = []
+ for upload in db.session.execute(select(Uploads).filter_by(key=user_code, yamlfile=filename, persistent=True)).scalars():
+ files_to_save.append(upload.indexno)
+ if len(files_to_save) > 0:
+ something_added = False
+ if user_type == 'user':
+ for uploads_indexno in files_to_save:
+ existing_auth = db.session.execute(select(UploadsUserAuth).filter_by(user_id=the_user_id, uploads_indexno=uploads_indexno)).scalar()
+ if not existing_auth:
+ new_auth_record = UploadsUserAuth(user_id=the_user_id, uploads_indexno=uploads_indexno)
+ db.session.add(new_auth_record)
+ something_added = True
+ else:
+ for uploads_indexno in files_to_save:
+ existing_auth = db.session.execute(select(UploadsUserAuth).filter_by(temp_user_id=the_user_id, uploads_indexno=uploads_indexno)).scalar()
+ if not existing_auth:
+ new_auth_record = UploadsUserAuth(temp_user_id=the_user_id, uploads_indexno=uploads_indexno)
+ db.session.add(new_auth_record)
+ something_added = True
+ if something_added:
+ db.session.commit()
+ if do_delete:
+ db.session.execute(delete(UserDict).filter_by(key=user_code, filename=filename))
+ db.session.commit()
+ manage_tts_objects(1, user_code=user_code, filename=filename)
+ manage_global_objects(3, key=user_code, filename=filename)
+ manage_chat_logs(3, key=user_code, filename=filename)
+ manage_email_server_objects(2, user_code=user_code, filename=filename)
+ # server.delete_answer_json(user_code, filename, delete_all=True)
+
+
+def unattached_uid():
+ while True:
+ newname = random_alphanumeric(32)
+ existing_key = db.session.execute(select(UserDict).filter_by(key=newname)).first()
+ if existing_key:
+ continue
+ return newname
+
+
+def decrypt_session(secret, user_code=None, filename=None):
+ # logmessage("decrypt_session: user_code is " + str(user_code) + " and filename is " + str(filename))
+ nowtime = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
+ if user_code is None or filename is None or secret is None:
+ return
+ manage_tts_objects(2, user_code=user_code, filename=filename, secret=secret)
+ for record in db.session.execute(select(UserDict).filter_by(key=user_code, filename=filename, encrypted=True).order_by(UserDict.indexno).with_for_update()).scalars():
+ the_dict = decrypt_dictionary(record.dictionary, secret)
+ record.dictionary = pack_dictionary(the_dict)
+ record.encrypted = False
+ record.modtime = nowtime
+ db.session.commit()
+ manage_chat_logs(4, key=user_code, filename=filename, secret=secret)
+
+
+def encrypt_session(secret, user_code=None, filename=None):
+ # logmessage("encrypt_session: user_code is " + str(user_code) + " and filename is " + str(filename))
+ nowtime = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
+ if user_code is None or filename is None or secret is None:
+ return
+ manage_tts_objects(3, user_code=user_code, filename=filename, secret=secret)
+ for record in db.session.execute(select(UserDict).filter_by(key=user_code, filename=filename, encrypted=False).order_by(UserDict.indexno).with_for_update()).scalars():
+ the_dict = unpack_dictionary(record.dictionary)
+ record.dictionary = encrypt_dictionary(the_dict, secret)
+ record.encrypted = True
+ record.modtime = nowtime
+ db.session.commit()
+ manage_chat_logs(5, key=user_code, filename=filename, secret=secret)
+
+
+def substitute_secret(oldsecret, newsecret, user=None, to_convert=None):
+ if user is None:
+ user = current_user
+ device_id = request.cookies.get('ds', None)
+ if device_id is None:
+ device_id = random_string(16)
+ the_current_info = current_info(yaml=None, req=request, action=None, session_info=None, secret=oldsecret, device_id=device_id)
+ this_thread.current_info = the_current_info
+ temp_user = session.get('tempuser', None)
+ # logmessage("substitute_secret: " + repr(oldsecret) + " and " + repr(newsecret) + " and temp_user is " + repr(temp_user))
+ if oldsecret in ('None', newsecret):
+ # logmessage("substitute_secret: returning new secret without doing anything")
+ return newsecret
+ # logmessage("substitute_secret: continuing")
+ if temp_user is not None:
+ temp_user_info = {'email': None, 'the_user_id': 't' + str(temp_user), 'theid': temp_user, 'roles': []}
+ the_current_info['user'] = temp_user_info
+ manage_global_objects(7, user_id=user.id, oldsecret=oldsecret, newsecret=newsecret)
+ if to_convert is None:
+ to_do = set()
+ if 'i' in session and 'uid' in session: # TEMPORARY
+ get_session(session['i'])
+ if 'sessions' in session:
+ for filename, info in session['sessions'].items():
+ to_do.add((filename, info['uid']))
+ for the_record in db.session.execute(select(UserDict.filename, UserDict.key).filter_by(user_id=user.id).group_by(UserDict.filename, UserDict.key)):
+ to_do.add((the_record.filename, the_record.key))
+ for the_record in db.session.execute(select(UserDictKeys.filename, UserDictKeys.key).join(UserDict, and_(UserDictKeys.filename == UserDict.filename, UserDictKeys.key == UserDict.key)).where(and_(UserDictKeys.user_id == user.id)).group_by(UserDictKeys.filename, UserDictKeys.key)):
+ to_do.add((the_record.filename, the_record.key))
+ else:
+ to_do = set(to_convert)
+ for (filename, user_code) in to_do:
+ the_current_info['yaml_filename'] = filename
+ the_current_info['session'] = user_code
+ the_current_info['encrypted'] = True
+ # obtain_lock(user_code, filename)
+ # logmessage("substitute_secret: filename is " + str(filename) + " and key is " + str(user_code))
+ manage_tts_objects(4, user_code=user_code, filename=filename, oldsecret=oldsecret, newsecret=newsecret)
+ manage_global_objects(6, key=user_code, filename=filename, oldsecret=oldsecret, newsecret=newsecret)
+ for record in db.session.execute(select(UserDict).filter_by(key=user_code, filename=filename, encrypted=True).order_by(UserDict.indexno).with_for_update()).scalars():
+ # logmessage("substitute_secret: record was encrypted")
+ try:
+ the_dict = decrypt_dictionary(record.dictionary, oldsecret)
+ except:
+ logmessage("substitute_secret: error decrypting dictionary for filename " + filename + " and uid " + user_code)
+ continue
+ if not isinstance(the_dict, dict):
+ logmessage("substitute_secret: dictionary was not a dict for filename " + filename + " and uid " + user_code)
+ continue
+ if temp_user:
+ try:
+ old_entry = the_dict['_internal']['user_local']['t' + str(temp_user)]
+ del the_dict['_internal']['user_local']['t' + str(temp_user)]
+ the_dict['_internal']['user_local'][str(user.id)] = old_entry
+ except:
+ pass
+ record.dictionary = encrypt_dictionary(the_dict, newsecret)
+ db.session.commit()
+ if temp_user:
+ for record in db.session.execute(select(UserDict).filter_by(key=user_code, filename=filename, encrypted=False).order_by(UserDict.indexno).with_for_update()).scalars():
+ try:
+ the_dict = unpack_dictionary(record.dictionary)
+ except:
+ logmessage("substitute_secret: error unpacking dictionary for filename " + filename + " and uid " + user_code)
+ continue
+ if not isinstance(the_dict, dict):
+ logmessage("substitute_secret: dictionary was not a dict for filename " + filename + " and uid " + user_code)
+ continue
+ try:
+ old_entry = the_dict['_internal']['user_local']['t' + str(temp_user)]
+ del the_dict['_internal']['user_local']['t' + str(temp_user)]
+ the_dict['_internal']['user_local'][str(user.id)] = old_entry
+ except:
+ pass
+ record.dictionary = pack_dictionary(the_dict)
+ db.session.commit()
+ manage_chat_logs(6, key=user_code, filename=filename, oldsecret=oldsecret, newsecret=newsecret)
+ # release_lock(user_code, filename)
+ manage_global_objects(7, user_id=user.id, oldsecret=oldsecret, newsecret=newsecret)
+ return newsecret
+
+@hookimpl(specname='server_get_session_variables')
+def get_session_variables(yaml_filename, session_id, secret, simplify, use_lock):
+ if secret is None:
+ secret = this_thread.current_info.get('secret', None)
+ # logmessage("get_session_variables: fetch_user_dict")
+ with lock_context(session_id, yaml_filename, use_lock=use_lock), global_context(copy_of_globals(this_thread)):
+ this_thread.current_info['yaml_filename'] = yaml_filename
+ try:
+ steps, user_dict, is_encrypted = fetch_user_dict(session_id, yaml_filename, secret=str(secret)) # pylint: disable=unused-variable
+ except BaseException as the_err:
+ raise DAException("Unable to decrypt interview dictionary") from the_err
+ if user_dict is None:
+ raise DAException("Unable to obtain interview dictionary.")
+ if simplify:
+ variables = serializable_dict(user_dict, include_internal=True)
+ # variables['_internal'] = serializable_dict(user_dict['_internal'])
+ return variables
+ return user_dict
+
+
+@hookimpl(specname='server_go_back_in_session')
+def go_back_in_session(yaml_filename, session_id, secret, return_question, use_lock, encode):
+ with lock_context(session_id, yaml_filename, use_lock=use_lock), global_context(copy_of_globals(this_thread)):
+ this_thread.current_info['yaml_filename'] = yaml_filename
+ try:
+ steps, user_dict, is_encrypted = fetch_user_dict(session_id, yaml_filename, secret=secret)
+ except Exception as err:
+ raise DAException("Unable to decrypt interview dictionary.") from err
+ if user_dict is None:
+ raise DAException("Unable to obtain interview dictionary.")
+ if steps == 1:
+ raise DAException("Cannot go back.")
+ old_user_dict = user_dict
+ steps, user_dict, is_encrypted = fetch_previous_user_dict(session_id, yaml_filename, secret)
+ if user_dict is None:
+ raise DAException("Unable to obtain interview dictionary.")
+ if return_question:
+ try:
+ data = get_question_data(yaml_filename, session_id, secret, use_lock=False, user_dict=user_dict, steps=steps, is_encrypted=is_encrypted, old_user_dict=old_user_dict, encode=encode)
+ except BaseException as the_err:
+ raise DAException("Problem getting current question") from the_err
+ else:
+ data = None
+ return data
+
+@hookimpl(specname="server_set_session_variables")
+def set_session_variables_adapter(yaml_filename, session_id, variables, secret, return_question, literal_variables, del_variables, question_name, event_list, advance_progress_meter, post_setting, use_lock, encode, process_objects):
+ return set_session_variables(yaml_filename, session_id, variables, secret=secret, return_question=return_question, literal_variables=literal_variables, del_variables=del_variables, question_name=question_name, event_list=event_list, advance_progress_meter=advance_progress_meter, post_setting=post_setting, use_lock=use_lock, encode=encode, process_objects=process_objects)
+
+def set_session_variables(yaml_filename, session_id, variables, secret=None, return_question=False, literal_variables=None, del_variables=None, question_name=None, event_list=None, advance_progress_meter=False, post_setting=True, use_lock=False, encode=False, process_objects=False):
+ device_id = this_thread.current_info['user']['device_id']
+ session_uid = this_thread.current_info['user']['session_uid']
+ if secret is None:
+ secret = this_thread.current_info.get('secret', None)
+ with lock_context(session_id, yaml_filename, use_lock=use_lock), global_context(copy_of_globals(this_thread)), session_context():
+ this_thread.current_info['yaml_filename'] = yaml_filename
+ try:
+ steps, user_dict, is_encrypted = fetch_user_dict(session_id, yaml_filename, secret=secret)
+ except Exception as the_err:
+ raise DAException("Unable to decrypt interview dictionary.") from the_err
+ vars_set = set()
+ old_values = {}
+ if user_dict is None:
+ raise DAException("Unable to obtain interview dictionary.")
+ if process_objects:
+ variables = transform_json_variables(variables)
+ pre_assembly_necessary = False
+ for key, val in variables.items():
+ if contains_volatile.search(key):
+ pre_assembly_necessary = True
+ break
+ if pre_assembly_necessary is False and literal_variables is not None:
+ for key, val in literal_variables.items():
+ if contains_volatile.search(key):
+ pre_assembly_necessary = True
+ break
+ if pre_assembly_necessary is False and del_variables is not None:
+ for key in del_variables:
+ if contains_volatile.search(key):
+ pre_assembly_necessary = True
+ break
+ if pre_assembly_necessary:
+ interview = get_interview(yaml_filename)
+ if current_user.is_anonymous:
+ if not interview.allowed_to_access(is_anonymous=True):
+ raise DAException('Insufficient permissions to run this interview.')
+ else:
+ if not interview.allowed_to_access(has_roles=[role.name for role in current_user.roles]):
+ raise DAException('Insufficient permissions to run this interview.')
+ ci = current_info(yaml=yaml_filename, req=request, secret=secret, device_id=device_id, session_uid=session_uid)
+ ci['session'] = session_id
+ ci['encrypted'] = is_encrypted
+ ci['secret'] = secret
+ interview_status = InterviewStatus(current_info=ci)
+ try:
+ interview.assemble(user_dict, interview_status)
+ except BaseException as err:
+ raise DAException("Error processing session: " + err.__class__.__name__ + ": " + str(err)) from err
+ try:
+ for key, val in variables.items():
+ if illegal_variable_name(key):
+ raise DAException("Illegal value as variable name.")
+ if isinstance(val, (str, bool, int, float, NoneType)):
+ exec(str(key) + ' = ' + repr(val), user_dict)
+ else:
+ if key == '_xxxtempvarxxx':
+ continue
+ user_dict['_xxxtempvarxxx'] = copy.deepcopy(val)
+ exec(str(key) + ' = _xxxtempvarxxx', user_dict)
+ del user_dict['_xxxtempvarxxx']
+ process_set_variable(str(key), user_dict, vars_set, old_values)
+ except BaseException as the_err:
+ if '_xxxtempvarxxx' in user_dict:
+ del user_dict['_xxxtempvarxxx']
+ raise DAException("Problem setting variables:" + str(the_err)) from the_err
+ if literal_variables is not None:
+ exec('import docassemble.base.util', user_dict)
+ for key, val in literal_variables.items():
+ if illegal_variable_name(key):
+ raise DAException("Illegal value as variable name.")
+ exec(str(key) + ' = ' + val, user_dict)
+ process_set_variable(str(key), user_dict, vars_set, old_values)
+ if question_name is not None:
+ interview = get_interview(yaml_filename)
+ if current_user.is_anonymous:
+ if not interview.allowed_to_access(is_anonymous=True):
+ raise DAException('Insufficient permissions to run this interview.')
+ else:
+ if not interview.allowed_to_access(has_roles=[role.name for role in current_user.roles]):
+ raise DAException('Insufficient permissions to run this interview.')
+ if question_name in interview.questions_by_name:
+ interview.questions_by_name[question_name].mark_as_answered(user_dict)
+ else:
+ raise DAException("Problem marking question as completed")
+ if del_variables is not None:
+ try:
+ for key in del_variables:
+ if illegal_variable_name(key):
+ raise DAException("Illegal value as variable name.")
+ exec('del ' + str(key), user_dict)
+ except BaseException as the_err:
+ raise DAException("Problem deleting variables: " + str(the_err)) from the_err
+ session_uid = this_thread.current_info['user']['session_uid']
+ # if 'event_stack' in user_dict['_internal']:
+ # logmessage("Event stack starting as: " + repr(user_dict['_internal']['event_stack']))
+ # else:
+ # logmessage("No event stack.")
+ if event_list is not None and len(event_list) and 'event_stack' in user_dict['_internal'] and session_uid in user_dict['_internal']['event_stack'] and len(user_dict['_internal']['event_stack'][session_uid]):
+ for event_name in event_list:
+ if illegal_variable_name(event_name):
+ raise DAException("Illegal value as event name.")
+ if user_dict['_internal']['event_stack'][session_uid][0]['action'] == event_name:
+ user_dict['_internal']['event_stack'][session_uid].pop(0)
+ # logmessage("Popped " + str(event_name))
+ if len(user_dict['_internal']['event_stack'][session_uid]) == 0:
+ break
+ if len(vars_set) > 0 and 'event_stack' in user_dict['_internal'] and session_uid in user_dict['_internal']['event_stack'] and len(user_dict['_internal']['event_stack'][session_uid]):
+ for var_name in vars_set:
+ if user_dict['_internal']['event_stack'][session_uid][0]['action'] == var_name:
+ user_dict['_internal']['event_stack'][session_uid].pop(0)
+ # logmessage("Popped " + str(var_name))
+ if len(user_dict['_internal']['event_stack'][session_uid]) == 0:
+ break
+ if question_name is not None:
+ for var_name in vars_set:
+ if var_name in interview.invalidation_todo or var_name in interview.onchange_todo:
+ interview.invalidate_dependencies(var_name, user_dict, old_values)
+ try:
+ del user_dict['_internal']['dirty'][var_name]
+ except:
+ pass
+ # if 'event_stack' in user_dict['_internal']:
+ # logmessage("Event stack now: " + repr(user_dict['_internal']['event_stack']))
+ if post_setting:
+ steps += 1
+ if return_question:
+ try:
+ data = get_question_data(yaml_filename, session_id, secret, use_lock=False, user_dict=user_dict, steps=steps, is_encrypted=is_encrypted, post_setting=post_setting, advance_progress_meter=advance_progress_meter, encode=encode)
+ except BaseException as the_err:
+ raise DAException("Problem getting current question:" + str(the_err)) from the_err
+ else:
+ data = None
+ if not return_question:
+ save_user_dict(session_id, user_dict, yaml_filename, secret=secret, encrypt=is_encrypted, changed=post_setting, steps=steps)
+ if 'multi_user' in vars_set:
+ if user_dict.get('multi_user', False) is True and is_encrypted is True:
+ decrypt_session(secret, user_code=session_id, filename=yaml_filename)
+ is_encrypted = False
+ if user_dict.get('multi_user', False) is False and is_encrypted is False:
+ encrypt_session(secret, user_code=session_id, filename=yaml_filename)
+ is_encrypted = True
+ return data
+
+
+@hookimpl(specname="server_create_session")
+def create_new_interview(yaml_filename, secret, url_args, referer, req):
+ interview = get_interview(yaml_filename)
+ if current_user.is_anonymous:
+ if not interview.allowed_to_initiate(is_anonymous=True):
+ raise DAException('Insufficient permissions to run this interview.')
+ if not interview.allowed_to_access(is_anonymous=True):
+ raise DAException('Insufficient permissions to run this interview.')
+ else:
+ if (not current_user.has_role('admin')) and (not interview.allowed_to_initiate(has_roles=[role.name for role in current_user.roles])):
+ raise DAException('Insufficient permissions to run this interview.')
+ if not interview.allowed_to_access(has_roles=[role.name for role in current_user.roles]):
+ raise DAException('Insufficient permissions to run this interview.')
+ if req is None:
+ req = request
+ if secret is None:
+ secret = random_string(16)
+ with global_context(copy_of_globals(this_thread)), session_context():
+ session_id, user_dict = reset_session(yaml_filename, secret) # obtains a lock than needs to be released
+ add_referer(user_dict, referer=referer)
+ if url_args and (isinstance(url_args, dict) or (hasattr(url_args, 'instanceName') and hasattr(url_args, 'elements') and isinstance(url_args.elements, dict))):
+ for key, val in url_args.items():
+ if isinstance(val, str):
+ val = val.encode('unicode_escape').decode()
+ user_dict['url_args'][key] = val
+ device_id = this_thread.current_info['user']['device_id']
+ session_uid = this_thread.current_info['user']['session_uid']
+ ci = current_info(yaml=yaml_filename, req=req, secret=secret, device_id=device_id, session_uid=session_uid)
+ ci['session'] = session_id
+ ci['encrypted'] = True
+ ci['secret'] = secret
+ interview_status = InterviewStatus(current_info=ci)
+ interview_status.checkin = True
+ try:
+ interview.assemble(user_dict, interview_status)
+ except DAErrorMissingVariable:
+ pass
+ except BaseException as e:
+ release_lock(session_id, yaml_filename)
+ if hasattr(e, 'traceback'):
+ the_trace = e.traceback
+ else:
+ the_trace = traceback.format_exc()
+ raise DAException("create_new_interview: failure to assemble interview: " + e.__class__.__name__ + ": " + str(e) + "\n" + str(the_trace)) # pylint: disable=raise-missing-from
+ encrypted = not bool(user_dict.get('multi_user', False) is True)
+ save_user_dict(session_id, user_dict, yaml_filename, secret=secret, encrypt=encrypted, changed=False, steps=1)
+ save_user_dict_key(session_id, yaml_filename)
+ release_lock(session_id, yaml_filename)
+ return (encrypted, session_id)
+
+
+@hookimpl(specname='server_get_question_data')
+def get_question_data_adapter(yaml_filename, session_id, secret, use_lock, user_dict, steps, is_encrypted, old_user_dict, save, post_setting, advance_progress_meter, action, encode):
+ return get_question_data(yaml_filename, session_id, secret, use_lock=use_lock, user_dict=user_dict, steps=steps, is_encrypted=is_encrypted, old_user_dict=old_user_dict, save=save, post_setting=post_setting, advance_progress_meter=advance_progress_meter, action=action, encode=encode)
+
+def get_question_data(yaml_filename, session_id, secret, use_lock=True, user_dict=None, steps=None, is_encrypted=None, old_user_dict=None, save=True, post_setting=False, advance_progress_meter=False, action=None, encode=False):
+ with lock_context(session_id, yaml_filename, use_lock=use_lock):
+ with global_context(copy_of_globals(this_thread)), session_context():
+ interview = get_interview(yaml_filename)
+ if current_user.is_anonymous:
+ if not interview.allowed_to_access(is_anonymous=True):
+ raise DAException('Insufficient permissions to run this interview.')
+ else:
+ if not interview.allowed_to_access(has_roles=[role.name for role in current_user.roles]):
+ raise DAException('Insufficient permissions to run this interview.')
+ device_id = this_thread.current_info['user']['device_id']
+ session_uid = this_thread.current_info['user']['session_uid']
+ ci = current_info(yaml=yaml_filename, req=request, secret=secret, device_id=device_id, action=action, session_uid=session_uid)
+ ci['session'] = session_id
+ ci['secret'] = secret
+ this_thread.current_info = ci
+ if user_dict is None:
+ try:
+ steps, user_dict, is_encrypted = fetch_user_dict(session_id, yaml_filename, secret=secret)
+ except BaseException as err:
+ raise DAException("Unable to obtain interview dictionary") from err
+ ci['encrypted'] = is_encrypted
+ interview_status = InterviewStatus(current_info=ci)
+ # interview_status.checkin = True
+ try:
+ interview.assemble(user_dict, interview_status=interview_status, old_user_dict=old_user_dict)
+ except DAErrorMissingVariable as err:
+ return {'questionType': 'undefined_variable', 'variable': err.variable, 'message_log': get_message_log()}
+ except BaseException as e:
+ raise DAException("get_question_data: failure to assemble interview: " + e.__class__.__name__ + ": " + str(e)) from e
+ save_status = this_thread.misc.get('save_status', SS_NEW)
+ try:
+ the_section = user_dict['nav'].get_section()
+ the_section_display = user_dict['nav'].get_section(display=True)
+ the_sections = user_dict['nav'].get_sections()
+ except:
+ the_section = None
+ the_section_display = None
+ the_sections = []
+ if advance_progress_meter:
+ if interview.use_progress_bar and interview_status.question.progress is None and save_status == SS_NEW:
+ advance_progress(user_dict, interview)
+ if interview.use_progress_bar and interview_status.question.progress is not None and (user_dict['_internal']['progress'] is None or interview.options.get('strict progress', False) or interview_status.question.progress > user_dict['_internal']['progress']):
+ user_dict['_internal']['progress'] = interview_status.question.progress
+ if save:
+ save_user_dict(session_id, user_dict, yaml_filename, secret=secret, encrypt=is_encrypted, changed=post_setting, steps=steps)
+ if user_dict.get('multi_user', False) is True and is_encrypted is True:
+ decrypt_session(secret, user_code=session_id, filename=yaml_filename)
+ is_encrypted = False
+ if user_dict.get('multi_user', False) is False and is_encrypted is False:
+ encrypt_session(secret, user_code=session_id, filename=yaml_filename)
+ is_encrypted = True
+ if interview_status.question.question_type == "response":
+ if hasattr(interview_status.question, 'response_code'):
+ resp_code = interview_status.question.response_code
+ else:
+ resp_code = 200
+ if hasattr(interview_status.question, 'all_variables'):
+ if hasattr(interview_status.question, 'include_internal'):
+ include_internal = interview_status.question.include_internal
+ else:
+ include_internal = False
+ response_to_send = make_response(dict_as_json(user_dict, include_internal=include_internal).encode('utf-8'), resp_code)
+ elif hasattr(interview_status.question, 'binaryresponse'):
+ response_to_send = make_response(interview_status.question.binaryresponse, resp_code)
+ else:
+ response_to_send = make_response(interview_status.question_text.encode('utf-8'), resp_code)
+ response_to_send.headers['Content-Type'] = interview_status.extras['content_type']
+ return {'questionType': 'response', 'response': response_to_send}
+ if interview_status.question.question_type == "sendfile":
+ if interview_status.question.response_file is not None:
+ the_path = interview_status.question.response_file.path()
+ else:
+ return jsonify_with_status("Could not send file because the response was None", 404)
+ if not os.path.isfile(the_path):
+ return jsonify_with_status("Could not send file because " + str(the_path) + " not found", 404)
+ response_to_send = custom_send_file(the_path, mimetype=interview_status.extras['content_type'])
+ response_to_send.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return {'questionType': 'response', 'response': response_to_send}
+ if interview_status.question.language != '*':
+ interview_language = interview_status.question.language
+ else:
+ interview_language = DEFAULT_LANGUAGE
+ title_info = interview.get_title(user_dict, status=interview_status, converter=lambda content, part: title_converter(content, part, interview_status))
+ interview_status.exit_url = title_info.get('exit url', None)
+ interview_status.exit_link = title_info.get('exit link', 'leave')
+ interview_status.exit_label = title_info.get('exit label', word('Exit'))
+ interview_status.title = title_info.get('full', default_title)
+ interview_status.display_title = title_info.get('logo', interview_status.title)
+ interview_status.tabtitle = title_info.get('tab', interview_status.title)
+ interview_status.short_title = title_info.get('short', title_info.get('full', default_short_title))
+ interview_status.display_short_title = title_info.get('short logo', title_info.get('logo', interview_status.short_title))
+ interview_status.title_url = title_info.get('title url', None)
+ interview_status.title_url_opens_in_other_window = title_info.get('title url opens in other window', True)
+ interview_status.nav_item = title_info.get('navigation bar html', '')
+ the_main_page_parts = main_page_parts.get(interview_language, main_page_parts.get('*'))
+ interview_status.pre = title_info.get('pre', the_main_page_parts['main page pre'])
+ interview_status.post = title_info.get('post', the_main_page_parts['main page post'])
+ interview_status.footer = title_info.get('footer', the_main_page_parts['main page footer'] or get_part('global footer'))
+ if interview_status.footer:
+ interview_status.footer = re.sub(r'?p.*?>', '', str(interview_status.footer), flags=re.IGNORECASE).strip()
+ if interview_status.footer == 'off':
+ interview_status.footer = ''
+ interview_status.submit = title_info.get('submit', the_main_page_parts['main page submit'])
+ interview_status.back = title_info.get('back button label', the_main_page_parts['main page back button label'] or interview_status.question.back())
+ interview_status.cornerback = title_info.get('corner back button label', the_main_page_parts['main page corner back button label'] or interview_status.question.cornerback())
+ if steps is None:
+ steps = user_dict['_internal']['steps']
+ allow_going_back = bool(interview_status.extras['can_go_back'] and (steps is None or (steps - user_dict['_internal']['steps_offset']) > 1))
+ data = {'browser_title': interview_status.tabtitle, 'exit_link': interview_status.exit_link, 'exit_url': interview_status.exit_url, 'exit_label': interview_status.exit_label, 'title': interview_status.title, 'display_title': interview_status.display_title, 'short_title': interview_status.short_title, 'lang': interview_language, 'steps': steps, 'allow_going_back': allow_going_back, 'message_log': get_message_log(), 'section': the_section, 'display_section': the_section_display, 'sections': the_sections}
+ if allow_going_back:
+ data['cornerBackButton'] = interview_status.cornerback
+ data.update(interview_status.as_data(user_dict, encode=encode))
+ if 'source' in data:
+ data['source']['varsLink'] = url_for('develop.get_variables', i=yaml_filename)
+ data['source']['varsLabel'] = word('Show variables and values')
+ # if interview_status.question.question_type == "review" and len(interview_status.question.fields_used):
+ # next_action_review = {'action': list(interview_status.question.fields_used)[0], 'arguments': {}}
+ # else:
+ # next_action_review = None
+ if 'reload_after' in interview_status.extras:
+ reload_after = 1000 * int(interview_status.extras['reload_after'])
+ else:
+ reload_after = 0
+ # if next_action_review:
+ # data['next_action'] = next_action_review
+ data['interview_options'] = interview.options
+ if reload_after and reload_after > 0:
+ data['reload_after'] = reload_after
+ for key in list(data.keys()):
+ if key == "_question_name":
+ data['questionName'] = data[key]
+ del data[key]
+ elif key.startswith('_'):
+ del data[key]
+ data['menu'] = {'items': []}
+ menu_items = data['menu']['items']
+ if 'menu_items' in interview_status.extras:
+ if not isinstance(interview_status.extras['menu_items'], list):
+ menu_items.append({'anchor': word("Error: menu_items is not a Python list")})
+ elif len(interview_status.extras['menu_items']) > 0:
+ for menu_item in interview_status.extras['menu_items']:
+ if not (isinstance(menu_item, dict) and 'url' in menu_item and 'label' in menu_item):
+ menu_items.append({'anchor': word("Error: menu item is not a Python dict with keys of url and label")})
+ else:
+ match_action = re.search(r'^\?action=([^\&]+)', menu_item['url'])
+ if match_action:
+ menu_items.append({'href': menu_item['url'], 'action': match_action.group(1), 'anchor': menu_item['label']})
+ else:
+ menu_items.append({'href': menu_item['url'], 'anchor': menu_item['label']})
+ if ALLOW_REGISTRATION:
+ sign_in_text = word('Sign in or sign up to save answers')
+ else:
+ sign_in_text = word('Sign in to save answers')
+ if daconfig.get('resume interview after login', False):
+ login_url = url_for('user.login', next=url_for('interview.index', i=yaml_filename))
+ else:
+ login_url = url_for('user.login')
+ if interview.consolidated_metadata.get('show login', SHOW_LOGIN):
+ if current_user.is_anonymous:
+ if len(menu_items) > 0:
+ data['menu']['top'] = {'anchor': word("Menu")}
+ menu_items.append({'href': login_url, 'anchor': sign_in_text})
+ else:
+ data['menu']['top'] = {'href': login_url, 'anchor': sign_in_text}
+ else:
+ if len(menu_items) == 0 and interview.options.get('hide standard menu', False):
+ data['menu']['top'] = {'anchor': (current_user.email if current_user.email else re.sub(r'.*\$', '', current_user.social_id))}
+ else:
+ data['menu']['top'] = {'anchor': current_user.email if current_user.email else re.sub(r'.*\$', '', current_user.social_id)}
+ if not interview.options.get('hide standard menu', False):
+ if current_user.has_role('admin', 'developer') and interview.debug:
+ menu_items.append({'href': '#source', 'title': word("How this question came to be asked"), 'anchor': word('Source')})
+ if current_user.has_role('admin', 'advocate') and current_app.config['ENABLE_MONITOR']:
+ menu_items.append({'href': url_for('monitor.monitor'), 'anchor': word('Monitor')})
+ if current_user.has_role('admin', 'developer', 'trainer'):
+ menu_items.append({'href': url_for('ml.train'), 'anchor': word('Train')})
+ if current_user.has_role('admin', 'developer'):
+ if current_app.config['ALLOW_UPDATES']:
+ menu_items.append({'href': url_for('packages.update_package'), 'anchor': word('Package Management')})
+ if current_app.config['ALLOW_LOG_VIEWING']:
+ menu_items.append({'href': url_for('logs.logs'), 'anchor': word('Logs')})
+ if current_app.config['ENABLE_PLAYGROUND']:
+ menu_items.append({'href': url_for('develop.playground_page'), 'anchor': word('Playground')})
+ menu_items.append({'href': url_for('develop.utilities'), 'anchor': word('Utilities')})
+ if current_user.has_role('admin', 'advocate') or current_user.can_do('access_user_info'):
+ menu_items.append({'href': url_for('users.user_list'), 'anchor': word('User List')})
+ if current_user.has_role('admin') and current_app.config['ALLOW_CONFIGURATION_EDITING']:
+ menu_items.append({'href': url_for('admin.config_page'), 'anchor': word('Configuration')})
+ if current_app.config['SHOW_DISPATCH']:
+ menu_items.append({'href': url_for('admin.interview_start'), 'anchor': word('Available Interviews')})
+ for item in current_app.config['ADMIN_INTERVIEWS']:
+ if item.can_use() and item.is_not(this_thread.current_info.get('yaml_filename', '')):
+ menu_items.append({'href': item.get_url(), 'anchor': item.get_title(get_language())})
+ if current_app.config['SHOW_MY_INTERVIEWS'] or current_user.has_role('admin'):
+ menu_items.append({'href': url_for('admin.interview_list'), 'anchor': word('My Interviews')})
+ if current_user.has_role('admin', 'developer'):
+ menu_items.append({'href': url_for('users.user_profile_page'), 'anchor': word('Profile')})
+ else:
+ if current_app.config['SHOW_PROFILE'] or current_user.has_role('admin'):
+ menu_items.append({'href': url_for('users.user_profile_page'), 'anchor': word('Profile')})
+ else:
+ menu_items.append({'href': url_for('user.change_password'), 'anchor': word('Change Password')})
+ menu_items.append({'href': url_for('user.logout'), 'anchor': word('Sign Out')})
+ else:
+ if len(menu_items) > 0:
+ data['menu']['top'] = {'anchor': word("Menu")}
+ if not interview.options.get('hide standard menu', False):
+ menu_items.append({'href': exit_href(data=True), 'anchor': interview_status.exit_label})
+ else:
+ data['menu']['top'] = {'href': exit_href(data=True), 'anchor': interview_status.exit_label}
+ # logmessage("Ok returning")
+ return data
+
+
+@hookimpl(specname="server_run_action_in_session")
+def run_action_in_session(kwargs):
+ yaml_filename = kwargs.get('i', None)
+ session_id = kwargs.get('session', None)
+ secret = kwargs.get('secret', None)
+ action = kwargs.get('action', None)
+ persistent = true_or_false(kwargs.get('persistent', False))
+ overwrite = true_or_false(kwargs.get('overwrite', False))
+ readonly = true_or_false(kwargs.get('read_only', False))
+ if yaml_filename is None or session_id is None or action is None:
+ return {"status": "error", "message": "Parameters i, session, and action are required."}
+ secret = str(secret)
+ if 'arguments' in kwargs and kwargs['arguments'] is not None:
+ if isinstance(kwargs['arguments'], dict):
+ arguments = kwargs['arguments']
+ else:
+ try:
+ arguments = json.loads(kwargs['arguments'])
+ except:
+ return {"status": "error", "message": "Malformed arguments."}
+ if not isinstance(arguments, dict):
+ return {"status": "error", "message": "Arguments data is not a dict."}
+ else:
+ arguments = {}
+ device_id = this_thread.current_info['user']['device_id']
+ session_uid = this_thread.current_info['user']['session_uid']
+ ci = current_info(yaml=yaml_filename, req=request, action={'action': action, 'arguments': arguments}, secret=secret, device_id=device_id, session_uid=session_uid)
+ ci['session'] = session_id
+ ci['secret'] = secret
+ interview = get_interview(yaml_filename)
+ if current_user.is_anonymous:
+ if not interview.allowed_to_access(is_anonymous=True):
+ raise DAException('Insufficient permissions to run this interview.')
+ else:
+ if not interview.allowed_to_access(has_roles=[role.name for role in current_user.roles]):
+ raise DAException('Insufficient permissions to run this interview.')
+ with global_context(copy_of_globals(this_thread)), session_context():
+ this_thread.current_info = ci
+ if readonly:
+ this_thread.misc['save_status'] = SS_IGNORE
+ overwrite = False
+ else:
+ obtain_lock(session_id, yaml_filename)
+ try:
+ steps, user_dict, is_encrypted = fetch_user_dict(session_id, yaml_filename, secret=secret)
+ except:
+ if this_thread.misc.get('save_status', SS_NEW) != SS_IGNORE:
+ release_lock(session_id, yaml_filename)
+ return {"status": "error", "message": "Unable to obtain interview dictionary."}
+ ci['encrypted'] = is_encrypted
+ interview_status = InterviewStatus(current_info=ci)
+ if not persistent:
+ interview_status.checkin = True
+ changed = True
+ try:
+ interview.assemble(user_dict, interview_status)
+ except DAErrorMissingVariable:
+ if overwrite:
+ save_status = SS_OVERWRITE
+ changed = False
+ else:
+ save_status = this_thread.misc.get('save_status', SS_NEW)
+ if save_status == SS_NEW:
+ steps += 1
+ user_dict['_internal']['steps'] = steps
+ if save_status != SS_IGNORE:
+ save_user_dict(session_id, user_dict, yaml_filename, secret=secret, encrypt=is_encrypted, changed=changed, steps=steps)
+ if user_dict.get('multi_user', False) is True and is_encrypted is True:
+ is_encrypted = False
+ decrypt_session(secret, user_code=session_id, filename=yaml_filename)
+ if user_dict.get('multi_user', False) is False and is_encrypted is False:
+ encrypt_session(secret, user_code=session_id, filename=yaml_filename)
+ is_encrypted = True
+ release_lock(session_id, yaml_filename)
+ return {"status": "success"}
+ except BaseException as e:
+ if this_thread.misc.get('save_status', SS_NEW) != SS_IGNORE:
+ release_lock(session_id, yaml_filename)
+ return {"status": "error", "message": "api_session_action: failure to assemble interview: " + e.__class__.__name__ + ": " + str(e)}
+ if overwrite:
+ save_status = SS_OVERWRITE
+ changed = False
+ else:
+ save_status = this_thread.misc.get('save_status', SS_NEW)
+ if save_status == SS_NEW:
+ steps += 1
+ user_dict['_internal']['steps'] = steps
+ if save_status != SS_IGNORE:
+ save_user_dict(session_id, user_dict, yaml_filename, secret=secret, encrypt=is_encrypted, changed=changed, steps=steps)
+ if user_dict.get('multi_user', False) is True and is_encrypted is True:
+ is_encrypted = False
+ decrypt_session(secret, user_code=session_id, filename=yaml_filename)
+ if user_dict.get('multi_user', False) is False and is_encrypted is False:
+ encrypt_session(secret, user_code=session_id, filename=yaml_filename)
+ is_encrypted = True
+ release_lock(session_id, yaml_filename)
+ if interview_status.question.question_type == "response":
+ if hasattr(interview_status.question, 'all_variables'):
+ if hasattr(interview_status.question, 'include_internal'):
+ include_internal = interview_status.question.include_internal
+ else:
+ include_internal = False
+ response_to_send = make_response(dict_as_json(user_dict, include_internal=include_internal).encode('utf-8'), '200 OK')
+ elif hasattr(interview_status.question, 'binaryresponse'):
+ response_to_send = make_response(interview_status.question.binaryresponse, '200 OK')
+ else:
+ response_to_send = make_response(interview_status.question_text.encode('utf-8'), '200 OK')
+ response_to_send.headers['Content-Type'] = interview_status.extras['content_type']
+ return response_to_send
+ if interview_status.question.question_type == "sendfile":
+ if interview_status.question.response_file is not None:
+ the_path = interview_status.question.response_file.path()
+ else:
+ return jsonify_with_status("Could not send file because the response was None", 404)
+ if not os.path.isfile(the_path):
+ return jsonify_with_status("Could not send file because " + str(the_path) + " not found", 404)
+ response_to_send = custom_send_file(the_path, mimetype=interview_status.extras['content_type'])
+ response_to_send.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ return response_to_send
+ return {'status': 'success'}
+
+
+def refresh_or_continue(interview, post_data):
+ return_val = False
+ try:
+ if interview.questions_by_name[post_data['_question_name']].fields[0].choices[int(post_data['X211bHRpcGxlX2Nob2ljZQ'])]['key'].question_type in ('refresh', 'continue'):
+ return_val = True
+ except:
+ pass
+ return return_val
+
+
+def update_current_info_with_session_info(the_current_info, session_info):
+ if session_info is not None:
+ user_code = session_info['uid']
+ encrypted = session_info['encrypted']
+ else:
+ user_code = None
+ encrypted = True
+ the_current_info.update({'session': user_code, 'encrypted': encrypted})
+
+
+def remove_i_from_dict(the_dict):
+ the_dict = copy.copy(the_dict)
+ if 'i' in the_dict:
+ del the_dict['i']
+ return the_dict
+
+
+def standard_app_values():
+ return {
+ "daThicknessScalingFactor": daconfig.get("signature pen thickness scaling factor"),
+ "daCsrf": generate_csrf(),
+ "daComboboxButtonLabel": word("Dropdown"),
+ "daInputBox": word("Input box"),
+ "daNotificationContainer": NOTIFICATION_CONTAINER,
+ "daNotificationMessage": NOTIFICATION_MESSAGE,
+ "daImageToPreLoad": url_for('static', filename='app/chat.ico', v=da_version),
+ "daLiveHelpMessage": word("Get help through live chat by clicking here."),
+ "daLiveHelpMessagePhone": word("Click here to get help over the phone."),
+ "daNewChatMessage": word("New chat message"),
+ "daLiveHelpAvailableMessage": word("Live chat is available"),
+ "daScreenBeingControlled": word("Your screen is being controlled by an operator."),
+ "daScreenNoLongerBeingControlled": word("The operator is no longer controlling your screen."),
+ "daPathRoot": ROOT,
+ "daAreYouSure": word("Are you sure you want to delete this item?"),
+ "daOtherUser": word("other user"),
+ "daOtherUsers": word("other users"),
+ "daOperator": word("operator"),
+ "daOperators": word("operators"),
+ "daAllButtonClasses": current_app.config['BUTTON_STYLE'] + 'primary ' + current_app.config['BUTTON_STYLE'] + 'info ' + current_app.config['BUTTON_STYLE'] + 'warning ' + current_app.config['BUTTON_STYLE'] + 'danger ' + current_app.config['BUTTON_STYLE'] + 'secondary',
+ "daButtonStyle": current_app.config['BUTTON_STYLE'],
+ "daCurrencyDecimalPlaces": daconfig.get('currency decimal places', 2),
+ "daSecureCookies": bool(current_app.config['SESSION_COOKIE_SECURE']),
+ "daEmailAddressRequired": word("An e-mail address is required."),
+ "daNeedCompleteEmail": word("You need to enter a complete e-mail address."),
+ "daToggleWord": word("Toggle")
+ }
+
+
+
+
+def get_history(interview, interview_status):
+ output = ''
+ has_question = bool(hasattr(interview_status, 'question'))
+ the_index = 0
+ seeking_len = len(interview_status.seeking)
+ if seeking_len:
+ starttime = interview_status.seeking[0]['time']
+ seen_done = False
+ for stage in interview_status.seeking:
+ if seen_done:
+ output = ''
+ seen_done = False
+ the_index += 1
+ if the_index < seeking_len and 'reason' in interview_status.seeking[the_index] and interview_status.seeking[the_index]['reason'] in ('asking', 'running') and interview_status.seeking[the_index]['question'] is stage['question'] and 'question' in stage and 'reason' in stage and stage['reason'] == 'considering':
+ continue
+ the_time = " at %.5fs" % (stage['time'] - starttime)
+ if 'question' in stage and 'reason' in stage and (has_question is False or the_index < (seeking_len - 1) or stage['question'] is not interview_status.question):
+ if stage['reason'] == 'initial':
+ output += " Ran initial code" + the_time + "
\n"
+ elif stage['reason'] == 'mandatory question':
+ output += " Tried to ask mandatory question" + the_time + "
\n"
+ elif stage['reason'] == 'mandatory code':
+ output += " Tried to run mandatory code" + the_time + "
\n"
+ elif stage['reason'] == 'asking':
+ output += " Tried to ask question" + the_time + "
\n"
+ elif stage['reason'] == 'running':
+ output += " Tried to run block" + the_time + "
\n"
+ elif stage['reason'] == 'considering':
+ output += " Considered using block" + the_time + "
\n"
+ elif stage['reason'] == 'objects from file':
+ output += " Tried to load objects from file" + the_time + "
\n"
+ elif stage['reason'] == 'data':
+ output += " Tried to load data" + the_time + "
\n"
+ elif stage['reason'] == 'objects':
+ output += " Tried to load objects" + the_time + "
\n"
+ elif stage['reason'] == 'result of multiple choice':
+ output += " Followed the result of multiple choice selection" + the_time + "
\n"
+ if stage['question'].from_source.path != interview.source.path and stage['question'].from_source.path is not None:
+ output += ' (' + word('from') + ' ' + stage['question'].from_source.path + ")
\n"
+ if (not hasattr(stage['question'], 'source_code')) or stage['question'].source_code is None:
+ output += word('(embedded question, source code not available)')
+ else:
+ output += highlight(stage['question'].source_code, YamlLexer(), HtmlFormatter(cssclass='highlight dahighlight'))
+ elif 'variable' in stage:
+ output += ' Needed definition of ' + str(stage['variable']) + "" + the_time + "
\n"
+ elif 'done' in stage:
+ output += " Completed processing" + the_time + "
\n"
+ seen_done = True
+ return output
+
+
+def read_fields(filename, orig_file_name, input_format, output_format):
+ if output_format == 'yaml':
+ if input_format == 'pdf':
+ fields = base_read_fields(filename)
+ fields_seen = set()
+ if fields is None:
+ raise DAException(word("Error: no fields could be found in the file"))
+ fields_output = "---\nquestion: " + word("Here is your document.") + "\nevent: " + 'some_event' + "\nattachment:" + "\n - name: " + os.path.splitext(orig_file_name)[0] + "\n filename: " + os.path.splitext(orig_file_name)[0] + "\n pdf template file: " + re.sub(r'[^A-Za-z0-9\-\_\. ]+', '_', orig_file_name) + "\n fields:\n"
+ for field, default, pageno, rect, field_type, export_value in fields:
+ if field not in fields_seen:
+ fields_output += ' - "' + str(field) + '": ' + sanitize(default) + "\n"
+ fields_seen.add(field)
+ fields_output += "---"
+ return fields_output
+ if input_format in ('docx', 'markdown'):
+ result = ''
+ if input_format == 'docx' and CAN_CONVERT_WORD:
+ result_file = word_to_markdown(filename, 'docx')
+ if result_file is None:
+ raise DAException(word("Error: no fields could be found in the file"))
+ with open(result_file.name, 'r', encoding='utf-8') as fp:
+ result = fp.read()
+ elif input_format == 'markdown':
+ with open(filename, 'r', encoding='utf-8') as fp:
+ result = fp.read()
+ fields = set()
+ for variable in re.findall(r'{{[pr] \s*([^\}\s]+)\s*}}', result):
+ fields.add(docx_variable_fix(variable))
+ for variable in re.findall(r'{{\s*([^\}\s]+)\s*}}', result):
+ fields.add(docx_variable_fix(variable))
+ for variable in re.findall(r'{%[a-z]* for [A-Za-z\_][A-Za-z0-9\_]* in *([^\} ]+) *%}', result):
+ fields.add(docx_variable_fix(variable))
+ if len(fields) == 0:
+ raise DAException(word("Error: no fields could be found in the file"))
+ fields_output = "---\nquestion: " + word("Here is your document.") + "\nevent: " + 'some_event' + "\nattachment:" + "\n - name: " + os.path.splitext(orig_file_name)[0] + "\n filename: " + os.path.splitext(orig_file_name)[0] + "\n docx template file: " + re.sub(r'[^A-Za-z0-9\-\_\. ]+', '_', orig_file_name) + "\n fields:\n"
+ for field in fields:
+ fields_output += ' "' + field + '": ' + "Something\n"
+ fields_output += "---"
+ return fields_output
+ if output_format == 'json':
+ if input_format == 'pdf':
+ default_text = word("something")
+ output = {'fields': [], 'default_values': {}, 'types': {}, 'locations': {}, 'export_values': {}}
+ fields = base_read_fields(filename)
+ if fields is not None:
+ fields_seen = set()
+ for field, default, pageno, rect, field_type, export_value in fields:
+ real_default = str(default)
+ if real_default == default_text:
+ real_default = ''
+ if field not in fields_seen:
+ output['fields'].append(str(field))
+ output['default_values'][field] = real_default
+ output['types'][field] = re.sub(r"'", r'', str(field_type))
+ output['locations'][field] = {'page': int(pageno), 'box': rect}
+ output['export_values'][field] = export_value
+ return json.dumps(output, sort_keys=True, indent=2)
+ if input_format in ('docx', 'markdown'):
+ if input_format == 'docx':
+ if CAN_CONVERT_WORD:
+ result_file = word_to_markdown(filename, 'docx')
+ else:
+ result_file = None
+ if result_file is None:
+ return json.dumps({'fields': []}, indent=2)
+ with open(result_file.name, 'r', encoding='utf-8') as fp:
+ result = fp.read()
+ elif input_format == 'markdown':
+ with open(filename, 'r', encoding='utf-8') as fp:
+ result = fp.read()
+ fields = set()
+ for variable in re.findall(r'{{ *([^\} ]+) *}}', result):
+ fields.add(docx_variable_fix(variable))
+ for variable in re.findall(r'{%[a-z]* for [A-Za-z\_][A-Za-z0-9\_]* in *([^\} ]+) *%}', result):
+ fields.add(docx_variable_fix(variable))
+ return json.dumps({'fields': list(fields)}, sort_keys=True, indent=2)
+ return None
+
+
+def get_corresponding_interview(the_package, the_file):
+ # logmessage("get_corresponding_interview: " + the_package + " " + the_file)
+ interview = None
+ if re.match(r'docassemble.playground[0-9]+', the_package):
+ separator = ':'
+ else:
+ separator = ':data/questions/'
+ for interview_file in (the_package + separator + the_file + '.yml', the_package + separator + the_file + '.yaml', the_package + separator + 'examples/' + the_file + '.yml'):
+ # logmessage("Looking for " + interview_file)
+ try:
+ interview = get_interview(interview_file)
+ break
+ except:
+ # logmessage("There was an exception looking for " + interview_file + ": " + str(the_err))
+ continue
+ return interview
+
+
+
+
+
+
+def user_interviews_filter(obj):
+ if isinstance(obj, DA.Condition):
+ leftside = user_interviews_filter(obj.leftside)
+ rightside = user_interviews_filter(obj.rightside)
+ if obj.operator == 'and':
+ return leftside & rightside
+ if obj.operator == 'xor':
+ return leftside ^ rightside
+ if obj.operator == 'or':
+ return leftside | rightside
+ if obj.operator == 'not':
+ return not_(leftside)
+ if obj.operator == 'le':
+ return leftside <= rightside
+ if obj.operator == 'ge':
+ return leftside >= rightside
+ if obj.operator == 'gt':
+ return leftside > rightside
+ if obj.operator == 'lt':
+ return leftside < rightside
+ if obj.operator == 'eq':
+ return leftside == rightside
+ if obj.operator == 'ne':
+ return leftside != rightside
+ if obj.operator == 'like':
+ return leftside.like(rightside)
+ if obj.operator == 'in':
+ return leftside.in_(rightside)
+ raise DAException("Operator not recognized")
+ if isinstance(obj, DA.Group):
+ items = [user_interviews_filter(item) for item in obj.items]
+ if obj.group_type == 'and':
+ return and_(*items)
+ if obj.group_type == 'or':
+ return or_(*items)
+ raise DAException("Group type not recognized")
+ if isinstance(obj, DA.Column):
+ if obj.name == 'indexno':
+ return UserDict.indexno
+ if obj.name == 'modtime':
+ return UserDict.modtime
+ if obj.name == 'filename':
+ return UserDictKeys.filename
+ if obj.name == 'key':
+ return UserDictKeys.key
+ if obj.name == 'encrypted':
+ return UserDict.encrypted
+ if obj.name == 'user_id':
+ return UserDictKeys.user_id
+ if obj.name == 'email':
+ return UserModel.email
+ if obj.name == 'first_name':
+ return UserModel.first_name
+ if obj.name == 'last_name':
+ return UserModel.last_name
+ if obj.name == 'country':
+ return UserModel.country
+ if obj.name == 'subdivisionfirst':
+ return UserModel.subdivisionfirst
+ if obj.name == 'subdivisionsecond':
+ return UserModel.subdivisionsecond
+ if obj.name == 'subdivisionthird':
+ return UserModel.subdivisionthird
+ if obj.name == 'organization':
+ return UserModel.organization
+ if obj.name == 'timezone':
+ return UserModel.timezone
+ if obj.name == 'language':
+ return UserModel.language
+ if obj.name == 'last_login':
+ return UserModel.last_login
+ raise DAException("Column " + repr(obj.name) + " not available")
+ return obj
+
+
+@hookimpl(specname='user_interviews')
+def user_interviews_adapter(user_id, secret, exclude_invalid, action, filename, session, tag, include_dict, delete_shared, admin, start_id, temp_user_id, query, minimal): # pylint: disable=redefined-outer-name
+ return user_interviews(user_id=user_id, secret=secret, exclude_invalid=exclude_invalid, action=action, filename=filename, session=session, tag=tag, include_dict=include_dict, delete_shared=delete_shared, admin=admin, start_id=start_id, temp_user_id=temp_user_id, query=query, minimal=minimal)
+
+def user_interviews(user_id=None, secret=None, exclude_invalid=True, action=None, filename=None, session=None, tag=None, include_dict=True, delete_shared=False, admin=False, start_id=None, temp_user_id=None, query=None, minimal=False): # pylint: disable=redefined-outer-name
+ # logmessage("user_interviews: user_id is " + str(user_id) + " and secret is " + str(secret))
+ if minimal is False:
+ if session is not None and user_id is None and temp_user_id is None and current_user.is_authenticated and not current_user.has_role_or_permission('admin', 'advocate', permissions=['access_sessions']):
+ user_id = current_user.id
+ elif user_id is None and (current_user.is_anonymous or not current_user.has_role_or_permission('admin', 'advocate', permissions=['access_sessions'])):
+ raise DAException('user_interviews: you do not have sufficient privileges to access information about other users')
+ if user_id is not None and admin is False and not (current_user.is_authenticated and (current_user.same_as(user_id) or current_user.has_role_or_permission('admin', 'advocate', permissions=['access_sessions']))):
+ raise DAException('user_interviews: you do not have sufficient privileges to access information about other users')
+ if action is not None and admin is False and not current_user.has_role_or_permission('admin', 'advocate', permissions=['edit_sessions']):
+ if user_id is None:
+ raise DAException("user_interviews: no user_id provided")
+ the_user = get_person(int(user_id), {})
+ if the_user is None:
+ raise DAException("user_interviews: user_id " + str(user_id) + " not valid")
+ if query is not None:
+ the_query = user_interviews_filter(query)
+ if action == 'delete_all':
+ sessions_to_delete = set()
+ if tag or query is not None:
+ start_id = None
+ while True:
+ (the_list, start_id) = user_interviews(user_id=user_id, secret=secret, filename=filename, session=session, tag=tag, include_dict=False, exclude_invalid=False, start_id=start_id, temp_user_id=temp_user_id, query=query, minimal=True)
+ for interview_info in the_list:
+ sessions_to_delete.add((interview_info['session'], interview_info['filename'], interview_info['user_id'], interview_info['temp_user_id']))
+ if start_id is None:
+ break
+ else:
+ where_clause = []
+ if temp_user_id is not None:
+ where_clause.append(UserDictKeys.temp_user_id == temp_user_id)
+ elif user_id is not None:
+ where_clause.append(UserDictKeys.user_id == user_id)
+ if filename is not None:
+ where_clause.append(UserDictKeys.filename == filename)
+ if session is not None:
+ where_clause.append(UserDictKeys.key == session)
+ interview_query = db.session.execute(select(UserDictKeys.filename, UserDictKeys.key, UserDictKeys.user_id, UserDictKeys.temp_user_id).where(*where_clause).group_by(UserDictKeys.filename, UserDictKeys.key, UserDictKeys.user_id, UserDictKeys.temp_user_id))
+ for interview_info in interview_query:
+ sessions_to_delete.add((interview_info.key, interview_info.filename, interview_info.user_id, interview_info.temp_user_id))
+ if user_id is not None:
+ if filename is None:
+ interview_query = db.session.execute(select(UserDict.filename, UserDict.key).where(UserDict.user_id == user_id).group_by(UserDict.filename, UserDict.key))
+ else:
+ interview_query = db.session.execute(select(UserDict.filename, UserDict.key).where(UserDict.user_id == user_id, UserDict.filename == filename).group_by(UserDict.filename, UserDict.key))
+ for interview_info in interview_query:
+ sessions_to_delete.add((interview_info.key, interview_info.filename, user_id, None))
+ logmessage("Deleting " + str(len(sessions_to_delete)) + " interviews")
+ if len(sessions_to_delete) > 0:
+ for session_id, yaml_filename, the_user_id, the_temp_user_id in sessions_to_delete:
+ manual_checkout(manual_session_id=session_id, manual_filename=yaml_filename, user_id=the_user_id, delete_session=True, temp_user_id=the_temp_user_id)
+ # obtain_lock(session_id, yaml_filename)
+ if the_user_id is None or delete_shared:
+ reset_user_dict(session_id, yaml_filename, user_id=the_user_id, temp_user_id=the_temp_user_id, force=True)
+ else:
+ reset_user_dict(session_id, yaml_filename, user_id=the_user_id, temp_user_id=the_temp_user_id)
+ # release_lock(session_id, yaml_filename)
+ return len(sessions_to_delete)
+ if action == 'delete':
+ if filename is None or session is None:
+ raise DAException("user_interviews: filename and session must be provided in order to delete interview")
+ manual_checkout(manual_session_id=session, manual_filename=filename, user_id=user_id, temp_user_id=temp_user_id, delete_session=True)
+ # obtain_lock(session, filename)
+ reset_user_dict(session, filename, user_id=user_id, temp_user_id=temp_user_id, force=delete_shared)
+ # release_lock(session, filename)
+ return True
+ if minimal:
+ the_timezone = None
+ elif admin is False and current_user and current_user.is_authenticated and current_user.timezone:
+ the_timezone = zoneinfo.ZoneInfo(current_user.timezone)
+ else:
+ the_timezone = zoneinfo.ZoneInfo(get_default_timezone())
+
+ interviews_length = 0
+ interviews = []
+
+ while True:
+ there_are_more = False
+ if temp_user_id is not None:
+ query_elements = [UserDict.indexno, UserDictKeys.user_id, UserDictKeys.temp_user_id, UserDictKeys.filename, UserDictKeys.key, UserModel.email]
+ subq_filter_elements = [UserDictKeys.temp_user_id == temp_user_id]
+ if include_dict:
+ query_elements.extend([UserDict.dictionary, UserDict.encrypted])
+ else:
+ query_elements.append(UserDict.modtime)
+ if filename is not None:
+ subq_filter_elements.append(UserDictKeys.filename == filename)
+ if session is not None:
+ subq_filter_elements.append(UserDictKeys.key == session)
+ if start_id is not None:
+ subq_filter_elements.append(UserDict.indexno > start_id)
+ subq = select(UserDictKeys.filename, UserDictKeys.key, db.func.max(UserDict.indexno).label('indexno')).join(UserDict, and_(UserDictKeys.filename == UserDict.filename, UserDictKeys.key == UserDict.key)) # pylint: disable=not-callable
+ if len(subq_filter_elements) > 0:
+ subq = subq.where(and_(*subq_filter_elements))
+ subq = subq.group_by(UserDictKeys.filename, UserDictKeys.key).subquery()
+ interview_query = select(*query_elements).select_from(subq.join(UserDict, subq.c.indexno == UserDict.indexno).join(UserDictKeys, and_(UserDict.filename == UserDictKeys.filename, UserDict.key == UserDictKeys.key, UserDictKeys.temp_user_id == temp_user_id)).outerjoin(UserModel, 0 == 1)) # pylint: disable=comparison-of-constants
+ if query is not None:
+ interview_query = interview_query.where(the_query)
+ interview_query = interview_query.order_by(UserDict.indexno)
+ elif user_id is not None:
+ query_elements = [UserDict.indexno, UserDictKeys.user_id, UserDictKeys.temp_user_id, UserDictKeys.filename, UserDictKeys.key, UserModel.email]
+ subq_filter_elements = [UserDictKeys.user_id == user_id]
+ if include_dict:
+ query_elements.extend([UserDict.dictionary, UserDict.encrypted])
+ else:
+ query_elements.append(UserDict.modtime)
+ if filename is not None:
+ subq_filter_elements.append(UserDictKeys.filename == filename)
+ if session is not None:
+ subq_filter_elements.append(UserDictKeys.key == session)
+ if start_id is not None:
+ subq_filter_elements.append(UserDict.indexno > start_id)
+ subq = select(UserDictKeys.filename, UserDictKeys.key, db.func.max(UserDict.indexno).label('indexno')).join(UserDict, and_(UserDictKeys.filename == UserDict.filename, UserDictKeys.key == UserDict.key)) # pylint: disable=not-callable
+ if len(subq_filter_elements) > 0:
+ subq = subq.where(and_(*subq_filter_elements))
+ subq = subq.group_by(UserDictKeys.filename, UserDictKeys.key).subquery()
+ interview_query = select(*query_elements).select_from(subq.join(UserDict, subq.c.indexno == UserDict.indexno).join(UserDictKeys, and_(UserDict.filename == UserDictKeys.filename, UserDict.key == UserDictKeys.key, UserDictKeys.user_id == user_id)).join(UserModel, UserDictKeys.user_id == UserModel.id))
+ if query is not None:
+ interview_query = interview_query.where(the_query)
+ interview_query = interview_query.order_by(UserDict.indexno)
+ else:
+ query_elements = [UserDict.indexno, UserDictKeys.user_id, UserDictKeys.temp_user_id, UserDict.filename, UserDict.key, UserModel.email]
+ subq_filter_elements = []
+ if include_dict:
+ query_elements.extend([UserDict.dictionary, UserDict.encrypted])
+ else:
+ query_elements.append(UserDict.modtime)
+ if filename is not None:
+ subq_filter_elements.append(UserDict.filename == filename)
+ if session is not None:
+ subq_filter_elements.append(UserDict.key == session)
+ if start_id is not None:
+ subq_filter_elements.append(UserDict.indexno > start_id)
+ subq = select(UserDict.filename, UserDict.key, db.func.max(UserDict.indexno).label('indexno')) # pylint: disable=not-callable
+ if len(subq_filter_elements) > 0:
+ subq = subq.where(and_(*subq_filter_elements))
+ subq = subq.group_by(UserDict.filename, UserDict.key).subquery()
+ interview_query = select(*query_elements).select_from(subq.join(UserDict, subq.c.indexno == UserDict.indexno).join(UserDictKeys, and_(UserDict.filename == UserDictKeys.filename, UserDict.key == UserDictKeys.key)).outerjoin(UserModel, and_(UserDictKeys.user_id == UserModel.id, UserModel.active == True))) # noqa: E712 # pylint: disable=singleton-comparison
+ if query is not None:
+ interview_query = interview_query.where(the_query)
+ interview_query = interview_query.order_by(UserDict.indexno)
+ interview_query = interview_query.limit(PAGINATION_LIMIT_PLUS_ONE)
+ stored_info = []
+ results_in_query = 0
+ for interview_info in db.session.execute(interview_query):
+ results_in_query += 1
+ if results_in_query == PAGINATION_LIMIT_PLUS_ONE:
+ there_are_more = True
+ break
+ # logmessage("filename is " + str(interview_info.filename) + " " + str(interview_info.key))
+ if session is not None and interview_info.key != session:
+ continue
+ if include_dict and interview_info.dictionary is None:
+ continue
+ if include_dict:
+ stored_info.append({'filename': interview_info.filename,
+ 'encrypted': interview_info.encrypted,
+ 'dictionary': interview_info.dictionary,
+ 'key': interview_info.key,
+ 'email': interview_info.email,
+ 'user_id': interview_info.user_id,
+ 'temp_user_id': interview_info.temp_user_id,
+ 'indexno': interview_info.indexno})
+ else:
+ stored_info.append({'filename': interview_info.filename,
+ 'modtime': interview_info.modtime,
+ 'key': interview_info.key,
+ 'email': interview_info.email,
+ 'user_id': interview_info.user_id,
+ 'temp_user_id': interview_info.temp_user_id,
+ 'indexno': interview_info.indexno})
+ for interview_info in stored_info:
+ if interviews_length == PAGINATION_LIMIT:
+ there_are_more = True
+ break
+ start_id = interview_info['indexno']
+ if minimal:
+ interviews.append({'filename': interview_info['filename'], 'session': interview_info['key'], 'user_id': interview_info['user_id'], 'temp_user_id': interview_info['temp_user_id']})
+ interviews_length += 1
+ continue
+ interview_title = {}
+ is_valid = True
+ interview_valid = True
+ try:
+ interview = get_interview(interview_info['filename'])
+ except:
+ if exclude_invalid:
+ continue
+ logmessage("user_interviews: unable to load interview file " + interview_info['filename'])
+ interview_title['full'] = word('Error: interview not found')
+ interview_valid = False
+ is_valid = False
+ # logmessage("Found old interview with title " + interview_title)
+ if include_dict:
+ if interview_info['encrypted']:
+ try:
+ dictionary = decrypt_dictionary(interview_info['dictionary'], secret)
+ except BaseException as the_err:
+ if exclude_invalid:
+ continue
+ try:
+ logmessage("user_interviews: unable to decrypt dictionary. " + str(the_err.__class__.__name__) + ": " + str(the_err))
+ except:
+ logmessage("user_interviews: unable to decrypt dictionary. " + str(the_err.__class__.__name__))
+ dictionary = fresh_dictionary()
+ dictionary['_internal']['starttime'] = None
+ dictionary['_internal']['modtime'] = None
+ is_valid = False
+ else:
+ try:
+ dictionary = unpack_dictionary(interview_info['dictionary'])
+ except BaseException as the_err:
+ if exclude_invalid:
+ continue
+ try:
+ logmessage("user_interviews: unable to unpack dictionary. " + str(the_err.__class__.__name__) + ": " + str(the_err))
+ except:
+ logmessage("user_interviews: unable to unpack dictionary. " + str(the_err.__class__.__name__))
+ dictionary = fresh_dictionary()
+ dictionary['_internal']['starttime'] = None
+ dictionary['_internal']['modtime'] = None
+ is_valid = False
+ if not isinstance(dictionary, dict):
+ logmessage("user_interviews: found a dictionary that was not a dictionary")
+ continue
+ if is_valid:
+ if include_dict:
+ interview_title = interview.get_title(dictionary)
+ tags = interview.get_tags(dictionary)
+ else:
+ interview_title = interview.get_title({'_internal': {}})
+ tags = interview.get_tags({'_internal': {}})
+ metadata = copy.deepcopy(interview.consolidated_metadata)
+ elif interview_valid:
+ interview_title = interview.get_title({'_internal': {}})
+ metadata = copy.deepcopy(interview.consolidated_metadata)
+ if include_dict:
+ tags = interview.get_tags(dictionary)
+ if 'full' not in interview_title:
+ interview_title['full'] = word("Interview answers cannot be decrypted")
+ else:
+ interview_title['full'] += ' - ' + word('interview answers cannot be decrypted')
+ else:
+ tags = interview.get_tags({'_internal': {}})
+ if 'full' not in interview_title:
+ interview_title['full'] = word('Unknown')
+ else:
+ interview_title['full'] = word('Error: interview not found and answers could not be decrypted')
+ metadata = {}
+ tags = set()
+ if include_dict:
+ if dictionary['_internal']['starttime'] and isinstance(dictionary['_internal']['starttime'], datetime.datetime):
+ utc_starttime = dictionary['_internal']['starttime']
+ starttime = nice_date_from_utc(dictionary['_internal']['starttime'], timezone=the_timezone)
+ else:
+ utc_starttime = None
+ starttime = ''
+ if dictionary['_internal']['modtime']:
+ utc_modtime = dictionary['_internal']['modtime']
+ modtime = nice_date_from_utc(dictionary['_internal']['modtime'], timezone=the_timezone)
+ else:
+ utc_modtime = None
+ modtime = ''
+ else:
+ utc_starttime = None
+ starttime = ''
+ utc_modtime = interview_info['modtime']
+ modtime = nice_date_from_utc(interview_info['modtime'], timezone=the_timezone)
+ if tag is not None and tag not in tags:
+ continue
+ out = {'filename': interview_info['filename'], 'session': interview_info['key'], 'modtime': modtime, 'starttime': starttime, 'utc_modtime': utc_modtime, 'utc_starttime': utc_starttime, 'title': interview_title.get('full', word('Untitled')), 'subtitle': interview_title.get('sub', None), 'valid': is_valid, 'metadata': metadata, 'tags': tags, 'email': interview_info['email'], 'user_id': interview_info['user_id'], 'temp_user_id': interview_info['temp_user_id']}
+ if include_dict:
+ out['dict'] = dictionary
+ out['encrypted'] = interview_info['encrypted']
+ interviews.append(out)
+ interviews_length += 1
+ if interviews_length == PAGINATION_LIMIT or results_in_query < PAGINATION_LIMIT_PLUS_ONE:
+ break
+ if there_are_more:
+ return (interviews, start_id)
+ return (interviews, None)
+
+
+def valid_date_key(x):
+ if x['dict']['_internal']['starttime'] is None:
+ return datetime.datetime.now()
+ return x['dict']['_internal']['starttime']
+
+
+def fix_secret(user=None, to_convert=None):
+ # logmessage("fix_secret starting")
+ if user is None:
+ user = current_user
+ password = str(request.form.get('password', request.form.get('new_password', None)))
+ if password is not None:
+ secret = str(request.cookies.get('secret', None))
+ newsecret = pad_to_16(MD5Hash(data=password).hexdigest())
+ if secret == 'None' or secret != newsecret:
+ # logmessage("fix_secret: calling substitute_secret with " + str(secret) + ' and ' + str(newsecret))
+ # logmessage("fix_secret: setting newsecret session")
+ session['newsecret'] = substitute_secret(str(secret), newsecret, user=user, to_convert=to_convert)
+ # else:
+ # logmessage("fix_secret: secrets are the same")
+ else:
+ logmessage("fix_secret: password not in request")
+
+
+def get_part(part, default=None):
+ if default is None:
+ default = str()
+ if part not in page_parts:
+ return default
+ if 'language' in session:
+ lang = session['language']
+ else:
+ lang = DEFAULT_LANGUAGE
+ if lang in page_parts[part]:
+ return page_parts[part][lang]
+ if lang != DEFAULT_LANGUAGE and DEFAULT_LANGUAGE in page_parts[part]:
+ return page_parts[part][DEFAULT_LANGUAGE]
+ if '*' in page_parts[part]:
+ return page_parts[part]['*']
+ return default
+
+
+def process_set_variable(field_name, user_dict, vars_set, old_values):
+ vars_set.add(field_name)
+ try:
+ old_values[field_name] = eval(field_name, user_dict)
+ except:
+ pass
diff --git a/docassemble_webapp/docassemble/webapp/interview/hooks.py b/docassemble_webapp/docassemble/webapp/interview/hooks.py
new file mode 100644
index 000000000..c4383f941
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/hooks.py
@@ -0,0 +1,6 @@
+from .config import main_page_parts
+from ..hooks.impl import hookimpl
+
+@hookimpl
+def get_main_page_parts():
+ return main_page_parts
diff --git a/docassemble_webapp/docassemble/webapp/interview/models.py b/docassemble_webapp/docassemble/webapp/interview/models.py
new file mode 100644
index 000000000..a16dbc50f
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/models.py
@@ -0,0 +1,43 @@
+from datetime import datetime
+from typing import Optional
+from sqlalchemy import Integer, String, Text, ForeignKey, Index, DateTime, true, Boolean
+from sqlalchemy.orm import Mapped, mapped_column
+from docassemble.webapp.database import dbtableprefix
+from docassemble.webapp.db_base import Base
+
+
+class UserDict(Base):
+ __tablename__ = dbtableprefix + "userdict"
+ __table_args__ = (
+ Index(dbtableprefix + 'ix_userdict_key_filename', 'key', 'filename'),
+ )
+ indexno: Mapped[int] = mapped_column(primary_key=True)
+ filename: Mapped[Optional[str]] = mapped_column(String(255), index=True)
+ key: Mapped[Optional[str]] = mapped_column(String(250), index=True)
+ dictionary: Mapped[Optional[str]] = mapped_column(Text)
+ user_id: Mapped[Optional[int]] = mapped_column(
+ Integer,
+ ForeignKey(dbtableprefix + "user.id", ondelete="CASCADE"),
+ )
+ encrypted: Mapped[bool] = mapped_column(Boolean, server_default=true())
+ modtime: Mapped[Optional[datetime]] = mapped_column(DateTime)
+
+
+class UserDictKeys(Base):
+ __tablename__ = dbtableprefix + "userdictkeys"
+ __table_args__ = (
+ Index(dbtableprefix + 'ix_userdictkeys_key_filename', 'key', 'filename'),
+ )
+ indexno: Mapped[int] = mapped_column(primary_key=True)
+ filename: Mapped[Optional[str]] = mapped_column(String(255), index=True)
+ key: Mapped[Optional[str]] = mapped_column(String(250), index=True)
+ user_id: Mapped[Optional[int]] = mapped_column(
+ Integer,
+ ForeignKey(dbtableprefix + "user.id", ondelete="CASCADE"),
+ index=True
+ )
+ temp_user_id: Mapped[Optional[int]] = mapped_column(
+ Integer,
+ ForeignKey(dbtableprefix + "tempuser.id", ondelete="CASCADE"),
+ index=True
+ )
diff --git a/docassemble_webapp/docassemble/webapp/interview/user_dict.py b/docassemble_webapp/docassemble/webapp/interview/user_dict.py
new file mode 100644
index 000000000..14db248e9
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/user_dict.py
@@ -0,0 +1,51 @@
+from sqlalchemy import delete, select, and_
+from docassemble.webapp.extensions import db
+from docassemble.webapp.utils.encryption import (
+ decrypt_dictionary,
+ unpack_dictionary,
+)
+from .models import UserDict
+
+
+# @elapsed('fetch_user_dict')
+def fetch_user_dict(user_code, filename, secret=None):
+ # logmessage("fetch_user_dict: user_code is " + str(user_code) + " and filename is " + str(filename))
+ user_dict = None
+ steps = 1
+ encrypted = True
+ subq = select(db.func.max(UserDict.indexno).label('indexno'), db.func.count(UserDict.indexno).label('cnt')).where(and_(UserDict.key == user_code, UserDict.filename == filename)).subquery() # pylint: disable=not-callable
+ stmt = select(UserDict.indexno, UserDict.dictionary, UserDict.encrypted, subq.c.cnt).join(subq, subq.c.indexno == UserDict.indexno)
+ result = db.session.execute(stmt)
+ for d in list(result):
+ # logmessage("fetch_user_dict: indexno is " + str(d.indexno))
+ if d.dictionary and isinstance(d.dictionary, str):
+ if d.encrypted:
+ # logmessage("fetch_user_dict: entry was encrypted")
+ user_dict = decrypt_dictionary(d.dictionary, secret)
+ # logmessage("fetch_user_dict: decrypted dictionary")
+ else:
+ # logmessage("fetch_user_dict: entry was not encrypted")
+ user_dict = unpack_dictionary(d.dictionary)
+ # logmessage("fetch_user_dict: unpacked dictionary")
+ encrypted = False
+ if d.cnt:
+ steps = d.cnt
+ break
+ return steps, user_dict, encrypted
+
+
+# @elapsed('user_dict_exists')
+# def user_dict_exists(user_code, filename):
+# result = db.session.execute(select(UserDict).where(and_(UserDict.key == user_code, UserDict.filename == filename))).first()
+# if result:
+# return True
+# return False
+
+
+# @elapsed('fetch_previous_user_dict')
+def fetch_previous_user_dict(user_code, filename, secret):
+ max_indexno = db.session.execute(select(db.func.max(UserDict.indexno)).where(and_(UserDict.key == user_code, UserDict.filename == filename))).scalar()
+ if max_indexno is not None:
+ db.session.execute(delete(UserDict).where(UserDict.indexno == max_indexno))
+ db.session.commit()
+ return fetch_user_dict(user_code, filename, secret=secret)
diff --git a/docassemble_webapp/docassemble/webapp/interview/views.py b/docassemble_webapp/docassemble/webapp/interview/views.py
new file mode 100644
index 000000000..db052b1ba
--- /dev/null
+++ b/docassemble_webapp/docassemble/webapp/interview/views.py
@@ -0,0 +1,3405 @@
+import copy
+import codecs
+import json
+import pickle
+import re
+import tempfile
+import os
+import base64
+from urllib.parse import quote as urllibquote
+import dateutil
+from flask import (
+ request,
+ redirect,
+ get_flashed_messages,
+ make_response,
+ session,
+ current_app,
+ flash,
+ jsonify,
+ abort,
+)
+from flask_login import logout_user, current_user
+from markupsafe import Markup
+from bs4 import BeautifulSoup
+from PIL import Image
+from flask_wtf.csrf import generate_csrf
+from pygments import highlight
+from pygments.formatters.html import HtmlFormatter
+from pygments.lexers import YamlLexer # pylint: disable=no-name-in-module
+import docassemble_flask_user
+from docassemble_textstat.textstat import textstat
+from docassemble.base.error import DAError, DAValidationError
+from docassemble.base.filter.html import markdown_to_html
+from docassemble.base.functions import (
+ custom_types,
+ get_message_log,
+ dict_as_json,
+ safe_json,
+)
+from docassemble.base.generate_key import random_string
+from docassemble.base.hooks import ensure_training_loaded, get_chat_log_internal, manage_tts_objects
+from docassemble.base.interview_cache import get_interview
+from docassemble.base.interview_source import interview_source_from_string
+from docassemble.base.language.control import get_language, get_dialect, get_voice
+from docassemble.base.language.words import word
+from docassemble.base.parse import (
+ parse_var_name,
+ InterviewStatus,
+ extension_of_doc_format,
+ ensure_object_exists,
+)
+from docassemble.base.save_status import SS_NEW, SS_OVERWRITE, SS_IGNORE
+from docassemble.base.standardformatter import as_html
+from docassemble.base.thread_context import (
+ user_dict_context,
+ old_user_dict_context,
+ this_thread,
+)
+from docassemble.base.util import DAFile, DAList, DAObject, zip_file
+from docassemble.webapp.config import (
+ CHECKIN_INTERVAL,
+ COOKIELESS_SESSIONS,
+ DEFAULT_DIALECT,
+ DEFAULT_LANGUAGE,
+ DEFAULT_VOICE,
+ DEFER,
+ NOTIFICATION_CONTAINER,
+ NOTIFICATION_MESSAGE,
+ PREVENT_DEMO,
+ REQUIRE_IDEMPOTENT,
+ SHOW_LOGIN,
+ STRICT_MODE,
+ TTS_ENABLED,
+ analytics_configured,
+ audio_mimetype_table,
+ daconfig,
+ default_short_title,
+ default_title,
+ exit_page,
+ final_default_yaml_filename,
+ ga_configured,
+ google_config,
+ reserved_argnames,
+ valid_voicerss_dialects,
+ voicerss_config,
+)
+from docassemble.webapp.daredis import r
+from docassemble.webapp.extensions import db
+from docassemble.webapp.files.file_number import get_new_file_number
+from docassemble.webapp.files.helpers import (
+ file_privilege_access,
+ file_set_attributes,
+ file_user_access,
+)
+from docassemble.webapp.files.savedfile import SavedFile
+from docassemble.webapp.screenreader import to_text
+from docassemble.webapp.sessions import (
+ clear_session,
+ update_session,
+ guess_yaml_filename,
+ get_session,
+)
+from docassemble.webapp.tasks.app import celery_app
+from docassemble.webapp.twilio.helpers import twilio_config
+from docassemble.webapp.users.models import TempUser
+from docassemble.webapp.utils.filenames import (
+ get_ext_and_mimetype,
+ secure_filename_unicode_ok,
+ secure_filename,
+)
+from docassemble.webapp.utils.encryption import (
+ decrypt_phrase,
+ encrypt_phrase,
+ pack_phrase,
+)
+from docassemble.webapp.utils.helpers import (
+ custom_send_file,
+ process_file,
+ key_requires_preassembly,
+ standard_scripts,
+ myb64unquote,
+ title_converter,
+ tidy_action,
+ current_info,
+ illegal_variable_name,
+ populate_social,
+ get_url_from_file_reference,
+ as_int,
+ redis_script,
+ reset_session,
+ process_bracket_expression,
+ repad,
+ jsonify_with_cache,
+ json64unquote,
+ match_inside_brackets,
+ safeid,
+ delete_session_info,
+ add_referer,
+ additional_scripts,
+ is_integer,
+ delete_session_for_interview,
+ MD5Hash,
+ make_navbar,
+ from_safeid,
+ standard_html_start,
+ additional_css,
+ make_response_wrapper,
+ match_brackets,
+ match_inside_and_outside_brackets,
+ do_refresh,
+ is_mobile_or_tablet,
+ navigation_bar,
+ do_redirect,
+ progress_bar,
+ true_or_false,
+ manual_checkout,
+ delete_session_sessions,
+)
+from docassemble.webapp.interview.dictionary import fresh_dictionary
+from docassemble.webapp.utils.hooks import url_for
+from docassemble.webapp.lock import obtain_lock, release_lock
+from docassemble.webapp.utils.logger import logmessage
+from .blueprint import interview_bp
+from .helpers import (
+ update_current_info_with_session_info,
+ refresh_or_continue,
+ standard_app_values,
+ get_existing_session,
+ remove_i_from_dict,
+ encrypt_session,
+ reset_user_dict,
+ get_history,
+ save_user_dict_key,
+ advance_progress,
+ decrypt_session,
+ save_user_dict,
+ get_part,
+ process_set_variable,
+)
+from .user_dict import fetch_user_dict, fetch_previous_user_dict
+from .config import main_page_parts, INDEX_PATH, HTML_INDEX_PATH
+
+@interview_bp.route("/checkout", methods=['POST'])
+def checkout():
+ try:
+ manual_checkout(manual_filename=request.args['i'])
+ except:
+ return jsonify(success=False)
+ return jsonify(success=True)
+
+
+@interview_bp.route("/checkin", methods=['POST', 'GET'])
+def checkin():
+ yaml_filename = request.args.get('i', None)
+ if yaml_filename is None:
+ return jsonify_with_cache(success=False)
+ session_info = get_session(yaml_filename)
+ if session_info is None:
+ return jsonify_with_cache(success=False)
+ session_id = session_info['uid']
+ if 'visitor_secret' in request.cookies:
+ secret = request.cookies['visitor_secret']
+ else:
+ secret = request.cookies.get('secret', None)
+ if secret is not None:
+ secret = str(secret)
+ if current_user.is_anonymous:
+ if 'tempuser' not in session:
+ return jsonify_with_cache(success=False)
+ the_user_id = 't' + str(session['tempuser'])
+ auth_user_id = None
+ temp_user_id = int(session['tempuser'])
+ elif current_user.is_authenticated:
+ auth_user_id = current_user.id
+ the_user_id = current_user.id
+ temp_user_id = None
+ else:
+ return jsonify_with_cache(success=True, action='reload')
+ the_current_info = current_info(yaml=yaml_filename, req=request, action=None, session_info=session_info, secret=secret, device_id=request.cookies.get('ds', None))
+ this_thread.current_info = the_current_info
+ if request.form.get('action', None) == 'chat_log':
+ # logmessage("checkin: fetch_user_dict1")
+ steps, user_dict, is_encrypted = fetch_user_dict(session_id, yaml_filename, secret=secret)
+ if user_dict is None or user_dict['_internal']['livehelp']['availability'] != 'available':
+ return jsonify_with_cache(success=False)
+ the_current_info['encrypted'] = is_encrypted
+ messages = get_chat_log_internal(user_dict['_internal']['livehelp']['mode'], yaml_filename, session_id, auth_user_id, temp_user_id, secret, auth_user_id, temp_user_id)
+ return jsonify_with_cache(success=True, messages=messages)
+ if request.form.get('action', None) == 'checkin':
+ commands = []
+ checkin_code = request.form.get('checkinCode', None)
+ do_action = request.form.get('do_action', None)
+ # logmessage("in checkin")
+ if do_action is not None:
+ parameters = {}
+ form_parameters = request.form.get('parameters', None)
+ read_only = true_or_false(request.form.get('read_only', False))
+ if form_parameters is not None:
+ parameters = json.loads(form_parameters)
+ # logmessage("Action was " + str(do_action) + " and parameters were " + repr(parameters))
+ if read_only:
+ this_thread.misc['save_status'] = SS_IGNORE
+ else:
+ obtain_lock(session_id, yaml_filename)
+ # logmessage("checkin: fetch_user_dict2")
+ steps, user_dict, is_encrypted = fetch_user_dict(session_id, yaml_filename, secret=secret)
+ the_current_info['encrypted'] = is_encrypted
+ interview = get_interview(yaml_filename)
+ interview_status = InterviewStatus(current_info=the_current_info)
+ interview_status.checkin = True
+ with user_dict_context(user_dict):
+ interview.assemble(user_dict, interview_status=interview_status)
+ interview_status.current_info.update({'action': do_action, 'arguments': parameters})
+ interview.assemble(user_dict, interview_status=interview_status)
+ if interview_status.question.question_type == "backgroundresponse":
+ the_response = interview_status.question.backgroundresponse
+ if isinstance(the_response, dict) and 'pargs' in the_response and isinstance(the_response['pargs'], list) and len(the_response['pargs']) == 2 and the_response['pargs'][1] in ('javascript', 'flash', 'refresh', 'fields'):
+ if the_response['pargs'][1] == 'refresh':
+ commands.append({'action': do_action, 'value': None, 'extra': the_response['pargs'][1]})
+ else:
+ commands.append({'action': do_action, 'value': safe_json(the_response['pargs'][0]), 'extra': the_response['pargs'][1]})
+ elif isinstance(the_response, list) and len(the_response) == 2 and the_response[1] in ('javascript', 'flash', 'refresh', 'fields'):
+ commands.append({'action': do_action, 'value': safe_json(the_response[0]), 'extra': the_response[1]})
+ elif isinstance(the_response, str) and the_response == 'refresh':
+ commands.append({'action': do_action, 'value': safe_json(None), 'extra': 'refresh'})
+ else:
+ commands.append({'action': do_action, 'value': safe_json(the_response), 'extra': 'backgroundresponse'})
+ elif interview_status.question.question_type == "template" and interview_status.question.target is not None:
+ commands.append({'action': do_action, 'value': {'target': interview_status.question.target, 'content': markdown_to_html(interview_status.question_text, trim=True)}, 'extra': 'backgroundresponse'})
+ save_status = this_thread.misc.get('save_status', SS_NEW)
+ if save_status != SS_IGNORE:
+ save_user_dict(session_id, user_dict, yaml_filename, secret=secret, encrypt=is_encrypted, steps=steps)
+ release_lock(session_id, yaml_filename)
+ peer_ok = False
+ help_ok = False
+ num_peers = 0
+ help_available = 0
+ session_info = get_session(yaml_filename)
+ old_chatstatus = session_info['chatstatus']
+ chatstatus = request.form.get('chatstatus', 'off')
+ if old_chatstatus != chatstatus:
+ update_session(yaml_filename, chatstatus=chatstatus)
+ obj = {'chatstatus': chatstatus, 'i': yaml_filename, 'uid': session_id, 'userid': the_user_id}
+ key = 'da:session:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)
+ call_forwarding_on = False
+ forwarding_phone_number = None
+ if twilio_config is not None:
+ forwarding_phone_number = twilio_config['name']['default'].get('number', None)
+ if forwarding_phone_number is not None:
+ call_forwarding_on = True
+ call_forwarding_code = None
+ call_forwarding_message = None
+ if call_forwarding_on:
+ for call_key in r.keys(re.sub(r'^da:session:uid:', 'da:phonecode:monitor:*:uid:', key)):
+ call_key = call_key.decode()
+ call_forwarding_code = r.get(call_key)
+ if call_forwarding_code is not None:
+ call_forwarding_code = call_forwarding_code.decode()
+ other_value = r.get('da:callforward:' + call_forwarding_code)
+ if other_value is None:
+ r.delete(call_key)
+ continue
+ other_value = other_value.decode()
+ remaining_seconds = r.ttl(call_key)
+ if remaining_seconds > 30:
+ call_forwarding_message = ''
+ break
+ chat_session_key = 'da:interviewsession:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)
+ potential_partners = []
+ if str(chatstatus) != 'off': # in ('waiting', 'standby', 'ringing', 'ready', 'on', 'hangup', 'observeonly'):
+ # logmessage("checkin: fetch_user_dict3")
+ steps, user_dict, is_encrypted = fetch_user_dict(session_id, yaml_filename, secret=secret)
+ the_current_info['encrypted'] = is_encrypted
+ if user_dict is None:
+ logmessage("checkin: error accessing dictionary for %s and %s" % (session_id, yaml_filename))
+ return jsonify_with_cache(success=False)
+ obj['chatstatus'] = chatstatus
+ obj['secret'] = secret
+ obj['encrypted'] = is_encrypted
+ obj['mode'] = user_dict['_internal']['livehelp']['mode']
+ if obj['mode'] in ('peer', 'peerhelp'):
+ peer_ok = True
+ if obj['mode'] in ('help', 'peerhelp'):
+ help_ok = True
+ obj['partner_roles'] = user_dict['_internal']['livehelp']['partner_roles']
+ if current_user.is_authenticated:
+ for attribute in ('email', 'confirmed_at', 'first_name', 'last_name', 'country', 'subdivisionfirst', 'subdivisionsecond', 'subdivisionthird', 'organization', 'timezone', 'language'):
+ obj[attribute] = str(getattr(current_user, attribute, None))
+ else:
+ obj['temp_user_id'] = temp_user_id
+ if help_ok and len(obj['partner_roles']) and not r.exists('da:block:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)):
+ pipe = r.pipeline()
+ for role in obj['partner_roles']:
+ role_key = 'da:chat:roletype:' + str(role)
+ pipe.set(role_key, 1)
+ pipe.expire(role_key, 2592000)
+ pipe.execute()
+ for role in obj['partner_roles']:
+ for the_key in r.keys('da:monitor:role:' + role + ':userid:*'):
+ user_id = re.sub(r'^.*:userid:', '', the_key.decode())
+ if user_id not in potential_partners:
+ potential_partners.append(user_id)
+ for the_key in r.keys('da:monitor:chatpartners:*'):
+ user_id = re.sub(r'^.*chatpartners:', '', the_key.decode())
+ if user_id not in potential_partners:
+ for chat_key in r.hgetall(the_key):
+ if chat_key.decode() == chat_session_key:
+ potential_partners.append(user_id)
+ if len(potential_partners) > 0:
+ if chatstatus == 'ringing':
+ lkey = 'da:ready:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)
+ # logmessage("Writing to " + str(lkey))
+ pipe = r.pipeline()
+ failure = True
+ for user_id in potential_partners:
+ for the_key in r.keys('da:monitor:available:' + str(user_id)):
+ pipe.rpush(lkey, the_key.decode())
+ failure = False
+ if peer_ok:
+ for the_key in r.keys('da:interviewsession:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:*'):
+ the_key = the_key.decode()
+ if the_key != chat_session_key:
+ pipe.rpush(lkey, the_key)
+ failure = False
+ if failure:
+ if peer_ok:
+ chatstatus = 'ready'
+ else:
+ chatstatus = 'waiting'
+ update_session(yaml_filename, chatstatus=chatstatus)
+ obj['chatstatus'] = chatstatus
+ else:
+ pipe.expire(lkey, 60)
+ pipe.execute()
+ chatstatus = 'ready'
+ update_session(yaml_filename, chatstatus=chatstatus)
+ obj['chatstatus'] = chatstatus
+ elif chatstatus == 'on':
+ if len(potential_partners) > 0:
+ already_connected_to_help = False
+ for user_id in potential_partners:
+ for the_key in r.hgetall('da:monitor:chatpartners:' + str(user_id)):
+ if the_key.decode() == chat_session_key:
+ already_connected_to_help = True
+ if not already_connected_to_help:
+ for user_id in potential_partners:
+ mon_sid = r.get('da:monitor:available:' + str(user_id))
+ if mon_sid is None:
+ continue
+ mon_sid = mon_sid.decode()
+ int_sid = r.get('da:interviewsession:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id))
+ if int_sid is None:
+ continue
+ int_sid = int_sid.decode()
+ r.publish(mon_sid, json.dumps({'messagetype': 'chatready', 'uid': session_id, 'i': yaml_filename, 'userid': the_user_id, 'secret': secret, 'sid': int_sid}))
+ r.publish(int_sid, json.dumps({'messagetype': 'chatpartner', 'sid': mon_sid}))
+ break
+ if chatstatus in ('waiting', 'hangup'):
+ chatstatus = 'standby'
+ update_session(yaml_filename, chatstatus=chatstatus)
+ obj['chatstatus'] = chatstatus
+ else:
+ if peer_ok:
+ if chatstatus == 'ringing':
+ lkey = 'da:ready:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)
+ pipe = r.pipeline()
+ failure = True
+ for the_key in r.keys('da:interviewsession:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:*'):
+ the_key = the_key.decode()
+ if the_key != chat_session_key:
+ pipe.rpush(lkey, the_key)
+ failure = False
+ if not failure:
+ pipe.expire(lkey, 6000)
+ pipe.execute()
+ chatstatus = 'ready'
+ update_session(yaml_filename, chatstatus=chatstatus)
+ obj['chatstatus'] = chatstatus
+ elif chatstatus in ('waiting', 'hangup'):
+ chatstatus = 'standby'
+ update_session(yaml_filename, chatstatus=chatstatus)
+ obj['chatstatus'] = chatstatus
+ else:
+ if chatstatus in ('standby', 'ready', 'ringing', 'hangup'):
+ chatstatus = 'waiting'
+ update_session(yaml_filename, chatstatus=chatstatus)
+ obj['chatstatus'] = chatstatus
+ if peer_ok:
+ for sess_key in r.keys('da:session:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:*'):
+ if sess_key.decode() != key:
+ num_peers += 1
+ help_available = len(potential_partners)
+ html_key = 'da:html:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)
+ if old_chatstatus != chatstatus:
+ html = r.get(html_key)
+ if html is not None:
+ html_obj = json.loads(html.decode())
+ if 'browser_title' in html_obj:
+ obj['browser_title'] = html_obj['browser_title']
+ obj['blocked'] = bool(r.exists('da:block:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)))
+ r.publish('da:monitor', json.dumps({'messagetype': 'sessionupdate', 'key': key, 'session': obj}))
+ else:
+ logmessage("checkin: the html was not found at " + str(html_key))
+ pipe = r.pipeline()
+ pipe.set(key, pickle.dumps(obj))
+ pipe.expire(key, 60)
+ pipe.expire(html_key, 60)
+ pipe.execute()
+ ocontrol_key = 'da:control:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)
+ ocontrol = r.get(ocontrol_key)
+ observer_control = not bool(ocontrol is None)
+ parameters = request.form.get('raw_parameters', None)
+ if parameters is not None:
+ key = 'da:input:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)
+ r.publish(key, parameters)
+ worker_key = 'da:worker:uid:' + str(session_id) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)
+ worker_len = r.llen(worker_key)
+ if worker_len > 0:
+ workers_inspected = 0
+ while workers_inspected <= worker_len:
+ worker_id = r.lpop(worker_key)
+ if worker_id is not None:
+ try:
+ result = celery_app.AsyncResult(id=worker_id)
+ if result.ready():
+ if result.result.__class__.__name__ == 'ReturnValue':
+ commands.append({'value': safe_json(result.result.value), 'extra': result.result.extra})
+ else:
+ r.rpush(worker_key, worker_id)
+ except BaseException as errstr:
+ logmessage("checkin: got error " + str(errstr))
+ r.rpush(worker_key, worker_id)
+ workers_inspected += 1
+ if peer_ok or help_ok:
+ return jsonify_with_cache(success=True, chat_status=chatstatus, num_peers=num_peers, help_available=help_available, phone=call_forwarding_message, observerControl=observer_control, commands=commands, checkin_code=checkin_code)
+ return jsonify_with_cache(success=True, chat_status=chatstatus, phone=call_forwarding_message, observerControl=observer_control, commands=commands, checkin_code=checkin_code)
+ return jsonify_with_cache(success=False)
+
+@interview_bp.route(INDEX_PATH, methods=['POST', 'GET'])
+def index(action_argument=None, refer=None):
+ # if refer is None and request.method == 'GET':
+ # setup_translation()
+ is_ajax = bool(request.method == 'POST' and 'ajax' in request.form and int(request.form['ajax']))
+ this_thread.misc['call'] = refer
+ return_fake_html = False
+ if (request.method == 'POST' and 'json' in request.form and as_int(request.form['json'])) or ('json' in request.args and as_int(request.args['json'])):
+ the_interface = 'json'
+ is_json = True
+ is_js = False
+ js_target = False
+ elif 'js_target' in request.args and request.args['js_target'] != '':
+ the_interface = 'web'
+ is_json = False
+ is_js = True
+ this_thread.misc['jsembed'] = request.args['js_target']
+ if is_ajax:
+ js_target = False
+ else:
+ js_target = request.args['js_target']
+ else:
+ the_interface = 'web'
+ is_json = False
+ is_js = False
+ js_target = False
+ if current_user.is_anonymous:
+ if 'tempuser' not in session:
+ new_temp_user = TempUser()
+ db.session.add(new_temp_user)
+ db.session.commit()
+ session['tempuser'] = new_temp_user.id
+ elif not current_user.is_authenticated:
+ response = do_redirect(url_for('user.login'), is_ajax, is_json, js_target)
+ response.set_cookie('remember_token', '', expires=0)
+ response.set_cookie('visitor_secret', '', expires=0)
+ response.set_cookie('secret', '', expires=0)
+ response.set_cookie('session', '', expires=0)
+ return response
+ elif 'user_id' not in session:
+ session['user_id'] = current_user.id
+ expire_visitor_secret = False
+ if 'visitor_secret' in request.cookies:
+ if 'session' in request.args:
+ secret = request.cookies.get('secret', None)
+ expire_visitor_secret = True
+ else:
+ secret = request.cookies['visitor_secret']
+ else:
+ secret = request.cookies.get('secret', None)
+ use_cache = int(request.args.get('cache', 1))
+ reset_interview = int(request.args.get('reset', 0))
+ new_interview = int(request.args.get('new_session', 0))
+ if secret is None:
+ secret = random_string(16)
+ set_cookie = True
+ set_device_id = True
+ else:
+ secret = str(secret)
+ set_cookie = False
+ set_device_id = False
+ device_id = request.cookies.get('ds', None)
+ if device_id is None:
+ device_id = random_string(16)
+ set_device_id = True
+ steps = 1
+ need_to_reset = False
+ if 'i' not in request.args and 'state' in request.args:
+ try:
+ yaml_filename = re.sub(r'\^.*', '', from_safeid(request.args['state']))
+ except:
+ yaml_filename = guess_yaml_filename()
+ else:
+ yaml_filename = request.args.get('i', guess_yaml_filename())
+ if yaml_filename is None:
+ if current_user.is_anonymous and not daconfig.get('allow anonymous access', True):
+ logmessage("Redirecting to login because no YAML filename provided and no anonymous access is allowed.")
+ return redirect(url_for('user.login'))
+ if len(daconfig['dispatch']) > 0:
+ logmessage("Redirecting to dispatch page because no YAML filename provided.")
+ return redirect(url_for('admin.interview_start'))
+ yaml_filename = final_default_yaml_filename
+ action = None
+ use_lock = True
+ if '_action' in request.form and 'in error' not in session:
+ action = tidy_action(json64unquote(request.form['_action']))
+ if true_or_false(request.form.get('_readonly', False)):
+ use_lock = False
+ this_thread.misc['save_status'] = SS_IGNORE
+ no_defs = True
+ elif 'action' in request.args and 'in error' not in session:
+ action = tidy_action(json64unquote(request.args['action']))
+ no_defs = True
+ elif action_argument:
+ action = tidy_action(action_argument)
+ no_defs = False
+ else:
+ no_defs = False
+ disregard_input = not bool(request.method == 'POST' and not no_defs)
+ if disregard_input:
+ post_data = {}
+ else:
+ post_data = request.form.copy()
+ if current_user.is_anonymous:
+ the_user_id = 't' + str(session['tempuser'])
+ else:
+ the_user_id = current_user.id
+ if '_track_location' in post_data and post_data['_track_location']:
+ the_location = json.loads(post_data['_track_location'])
+ else:
+ the_location = None
+ session_info = get_session(yaml_filename)
+ session_parameter = request.args.get('session', None)
+ the_current_info = current_info(yaml=yaml_filename, req=request, action=None, location=the_location, interface=the_interface, session_info=session_info, secret=secret, device_id=device_id)
+ this_thread.current_info = the_current_info
+ if session_info is None or reset_interview or new_interview:
+ was_new = True
+ if 'alt_session' in session and yaml_filename == session['alt_session'][0]:
+ session_parameter = session['alt_session'][1]
+ del session['alt_session']
+ if (PREVENT_DEMO) and (yaml_filename.startswith('docassemble.base:') or yaml_filename.startswith('docassemble.demo:')) and (current_user.is_anonymous or not (current_user.has_role('admin', 'developer') or current_user.can_do('demo_interviews'))):
+ raise DAError(word("Not authorized"), code=403)
+ if current_user.is_anonymous and not daconfig.get('allow anonymous access', True):
+ logmessage("Redirecting to login because no anonymous access allowed.")
+ return redirect(url_for('user.login', next=url_for('interview.index', **request.args)))
+ if yaml_filename.startswith('docassemble.playground'):
+ if not current_app.config['ENABLE_PLAYGROUND']:
+ raise DAError(word("Not authorized"), code=403)
+ else:
+ yaml_filename = re.sub(r':([^\/]+)$', r':data/questions/\1', yaml_filename)
+ this_thread.current_info['yaml_filename'] = yaml_filename
+ show_flash = False
+ interview = get_interview(yaml_filename)
+ if session_info is None and request.args.get('from_list', None) is None and not yaml_filename.startswith("docassemble.playground") and not yaml_filename.startswith("docassemble.base") and not yaml_filename.startswith("docassemble.demo") and SHOW_LOGIN and not new_interview and len(session['sessions']) > 0:
+ show_flash = True
+ if current_user.is_authenticated and current_user.has_role('admin', 'developer', 'advocate'):
+ show_flash = False
+ if session_parameter is None:
+ if show_flash:
+ if current_user.is_authenticated:
+ # word("Starting a new interview. To go back to your previous interview, go to My Interviews on the menu.")
+ message = "Starting a new interview. To go back to your previous interview, go to My Interviews on the menu."
+ else:
+ # word("Starting a new interview. To go back to your previous interview, log in to see a list of your interviews.")
+ message = "Starting a new interview. To go back to your previous interview, log in to see a list of your interviews."
+ if reset_interview and session_info is not None:
+ reset_user_dict(session_info['uid'], yaml_filename)
+ unique_sessions = interview.consolidated_metadata.get('sessions are unique', False)
+ if unique_sessions is not False and not current_user.is_authenticated:
+ delete_session_for_interview(yaml_filename)
+ flash(word("You need to be logged in to access this interview."), "info")
+ logmessage("Redirecting to login because sessions are unique.")
+ return redirect(url_for('user.login', next=url_for('interview.index', **request.args)))
+ if interview.consolidated_metadata.get('temporary session', False):
+ if session_info is not None:
+ reset_user_dict(session_info['uid'], yaml_filename)
+ if current_user.is_authenticated:
+ while True:
+ session_id, encrypted = get_existing_session(yaml_filename, secret)
+ if session_id:
+ reset_user_dict(session_id, yaml_filename)
+ else:
+ break
+ the_current_info['session'] = session_id
+ the_current_info['encrypted'] = encrypted
+ reset_interview = 1
+ if current_user.is_anonymous:
+ if (not interview.allowed_to_initiate(is_anonymous=True)) or (not interview.allowed_to_access(is_anonymous=True)):
+ delete_session_for_interview(yaml_filename)
+ flash(word("You need to be logged in to access this interview."), "info")
+ logmessage("Redirecting to login because anonymous user not allowed to access this interview.")
+ return redirect(url_for('user.login', next=url_for('interview.index', **request.args)))
+ elif not interview.allowed_to_initiate(has_roles=[role.name for role in current_user.roles]):
+ delete_session_for_interview(yaml_filename)
+ raise DAError(word("You are not allowed to access this interview."), code=403)
+ elif not interview.allowed_to_access(has_roles=[role.name for role in current_user.roles]):
+ raise DAError(word('You are not allowed to access this interview.'), code=403)
+ session_id = None
+ if reset_interview == 2:
+ delete_session_sessions()
+ if (not reset_interview) and (unique_sessions is True or (isinstance(unique_sessions, list) and len(unique_sessions) > 0 and current_user.has_role(*unique_sessions))):
+ session_id, encrypted = get_existing_session(yaml_filename, secret)
+ if session_id is None:
+ user_code, user_dict = reset_session(yaml_filename, secret)
+ add_referer(user_dict)
+ save_user_dict(user_code, user_dict, yaml_filename, secret=secret)
+ release_lock(user_code, yaml_filename)
+ need_to_reset = True
+ session_info = get_session(yaml_filename)
+ update_current_info_with_session_info(the_current_info, session_info)
+ else:
+ unique_sessions = interview.consolidated_metadata.get('sessions are unique', False)
+ if unique_sessions is not False and not current_user.is_authenticated:
+ delete_session_for_interview(yaml_filename)
+ session['alt_session'] = [yaml_filename, session_parameter]
+ flash(word("You need to be logged in to access this interview."), "info")
+ logmessage("Redirecting to login because sessions are unique.")
+ return redirect(url_for('user.login', next=url_for('interview.index', **request.args)))
+ if current_user.is_anonymous:
+ if (not interview.allowed_to_initiate(is_anonymous=True)) or (not interview.allowed_to_access(is_anonymous=True)):
+ delete_session_for_interview(yaml_filename)
+ session['alt_session'] = [yaml_filename, session_parameter]
+ flash(word("You need to be logged in to access this interview."), "info")
+ logmessage("Redirecting to login because anonymous user not allowed to access this interview.")
+ return redirect(url_for('user.login', next=url_for('interview.index', **request.args)))
+ elif not interview.allowed_to_initiate(has_roles=[role.name for role in current_user.roles]):
+ delete_session_for_interview(yaml_filename)
+ raise DAError(word("You are not allowed to access this interview."), code=403)
+ elif not interview.allowed_to_access(has_roles=[role.name for role in current_user.roles]):
+ raise DAError(word('You are not allowed to access this interview.'), code=403)
+ if reset_interview:
+ reset_user_dict(session_parameter, yaml_filename)
+ if reset_interview == 2:
+ delete_session_sessions()
+ user_code, user_dict = reset_session(yaml_filename, secret)
+ add_referer(user_dict)
+ save_user_dict(user_code, user_dict, yaml_filename, secret=secret)
+ release_lock(user_code, yaml_filename)
+ session_info = get_session(yaml_filename)
+ update_current_info_with_session_info(the_current_info, session_info)
+ need_to_reset = True
+ else:
+ session_info = update_session(yaml_filename, uid=session_parameter)
+ update_current_info_with_session_info(the_current_info, session_info)
+ need_to_reset = True
+ if show_flash:
+ if current_user.is_authenticated:
+ # word("Entering a different interview. To go back to your previous interview, go to My Interviews on the menu.")
+ message = "Entering a different interview. To go back to your previous interview, go to My Interviews on the menu."
+ else:
+ # word("Entering a different interview. To go back to your previous interview, log in to see a list of your interviews.")
+ message = "Entering a different interview. To go back to your previous interview, log in to see a list of your interviews."
+ if show_flash:
+ flash(word(message), 'info')
+ else:
+ was_new = False
+ if session_parameter is not None and not need_to_reset:
+ session_info = update_session(yaml_filename, uid=session_parameter)
+ update_current_info_with_session_info(the_current_info, session_info)
+ need_to_reset = True
+ user_code = session_info['uid']
+ encrypted = session_info['encrypted']
+ if use_lock:
+ obtain_lock(user_code, yaml_filename)
+ try:
+ steps, user_dict, is_encrypted = fetch_user_dict(user_code, yaml_filename, secret=secret)
+ except BaseException as the_err:
+ try:
+ logmessage("index: there was an exception " + str(the_err.__class__.__name__) + ": " + str(the_err) + " after fetch_user_dict with %s and %s, so we need to reset" % (user_code, yaml_filename))
+ except:
+ logmessage("index: there was an exception " + str(the_err.__class__.__name__) + " after fetch_user_dict with %s and %s, so we need to reset" % (user_code, yaml_filename))
+ if use_lock:
+ release_lock(user_code, yaml_filename)
+ logmessage("index: dictionary fetch failed")
+ clear_session(yaml_filename)
+ if session_parameter is not None:
+ redirect_url = daconfig.get('session error redirect url', None)
+ if isinstance(redirect_url, str) and redirect_url:
+ redirect_url = redirect_url.format(i=urllibquote(yaml_filename), error=urllibquote('answers_fetch_fail'))
+ logmessage("Session error because failure to get user dictionary.")
+ return do_redirect(redirect_url, is_ajax, is_json, js_target)
+ logmessage("Redirecting back to index because of failure to get user dictionary.")
+ response = do_redirect(url_for('interview.index', i=yaml_filename), is_ajax, is_json, js_target)
+ if session_parameter is not None:
+ flash(word("Unable to retrieve interview session. Starting a new session instead."), "error")
+ return response
+ if user_dict is None:
+ logmessage("index: no user_dict found after fetch_user_dict with %s and %s, so we need to reset" % (user_code, yaml_filename))
+ if use_lock:
+ release_lock(user_code, yaml_filename)
+ logmessage("index: dictionary fetch returned no results")
+ clear_session(yaml_filename)
+ redirect_url = daconfig.get('session error redirect url', None)
+ if isinstance(redirect_url, str) and redirect_url:
+ redirect_url = redirect_url.format(i=urllibquote(yaml_filename), error=urllibquote('answers_missing'))
+ logmessage("Session error because user dictionary was None.")
+ return do_redirect(redirect_url, is_ajax, is_json, js_target)
+ logmessage("Redirecting back to index because user dictionary was None.")
+ response = do_redirect(url_for('interview.index', i=yaml_filename), is_ajax, is_json, js_target)
+ flash(word("Unable to locate interview session. Starting a new session instead."), "error")
+ return response
+ if encrypted != is_encrypted:
+ update_session(yaml_filename, encrypted=is_encrypted)
+ encrypted = is_encrypted
+ if user_dict.get('multi_user', False) is True and encrypted is True:
+ encrypted = False
+ update_session(yaml_filename, encrypted=encrypted)
+ decrypt_session(secret, user_code=user_code, filename=yaml_filename)
+ if user_dict.get('multi_user', False) is False and encrypted is False:
+ encrypt_session(secret, user_code=user_code, filename=yaml_filename)
+ encrypted = True
+ update_session(yaml_filename, encrypted=encrypted)
+ the_current_info['encrypted'] = encrypted
+ if not session_info['key_logged']:
+ save_user_dict_key(user_code, yaml_filename)
+ update_session(yaml_filename, key_logged=True)
+ url_args_changed = False
+ old_url_args = {}
+ if len(request.args) > 0:
+ for argname in request.args:
+ if argname in reserved_argnames:
+ continue
+ if not url_args_changed:
+ old_url_args = copy.deepcopy(user_dict['url_args'])
+ url_args_changed = True
+ user_dict['url_args'][argname] = request.args.get(argname)
+ if url_args_changed:
+ if old_url_args == user_dict['url_args']:
+ url_args_changed = False
+ index_params = {'i': yaml_filename}
+ if analytics_configured:
+ for argname in request.args:
+ if argname in ('utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'):
+ index_params[argname] = request.args[argname]
+ if need_to_reset or set_device_id:
+ if use_cache == 0:
+ interview_source_from_string(yaml_filename).update_index()
+ response_wrapper = make_response_wrapper(set_cookie, secret, set_device_id, device_id, expire_visitor_secret)
+ else:
+ response_wrapper = None
+ interview = get_interview(yaml_filename)
+ interview_status = InterviewStatus(current_info=the_current_info, tracker=user_dict['_internal']['tracker'])
+ old_user_dict = None
+ if '_back_one' in post_data and steps > 1:
+ ok_to_go_back = True
+ if STRICT_MODE:
+ with user_dict_context(user_dict):
+ interview.assemble(user_dict, interview_status=interview_status)
+ if not interview_status.extras['can_go_back']:
+ ok_to_go_back = False
+ if ok_to_go_back:
+ action = None
+ the_current_info = current_info(yaml=yaml_filename, req=request, action=action, location=the_location, interface=the_interface, session_info=session_info, secret=secret, device_id=device_id)
+ this_thread.current_info = the_current_info
+ old_user_dict = user_dict
+ steps, user_dict, is_encrypted = fetch_previous_user_dict(user_code, yaml_filename, secret)
+ if encrypted != is_encrypted:
+ encrypted = is_encrypted
+ update_session(yaml_filename, encrypted=encrypted)
+ the_current_info['encrypted'] = encrypted
+ interview_status = InterviewStatus(current_info=the_current_info, tracker=user_dict['_internal']['tracker'])
+ post_data = {}
+ disregard_input = True
+ known_varnames = {}
+ all_invisible = False
+ if '_varnames' in post_data:
+ known_varnames = json.loads(myb64unquote(post_data['_varnames']))
+ if '_visible' in post_data and post_data['_visible'] != "":
+ visible_field_names = json.loads(myb64unquote(post_data['_visible']))
+ if len(visible_field_names) == 0 and '_question_name' in post_data and len(known_varnames) > 0:
+ all_invisible = True
+ else:
+ visible_field_names = []
+ known_varnames_visible = {}
+ for key, val in known_varnames.items():
+ if key in visible_field_names:
+ known_varnames_visible[key] = val
+ all_field_numbers = {}
+ field_numbers = {}
+ numbered_fields = {}
+ visible_fields = set()
+ raw_visible_fields = set()
+ for field_name in visible_field_names:
+ try:
+ m = re.search(r'(.*)(\[[^\]]+\])$', from_safeid(field_name))
+ if m:
+ if safeid(m.group(1)) in known_varnames:
+ visible_fields.add(safeid(from_safeid(known_varnames[safeid(m.group(1))]) + m.group(2)))
+ except:
+ pass
+ raw_visible_fields.add(field_name)
+ if field_name in known_varnames:
+ visible_fields.add(known_varnames[field_name])
+ else:
+ visible_fields.add(field_name)
+ for kv_key, kv_var in known_varnames.items():
+ try:
+ field_identifier = myb64unquote(kv_key)
+ m = re.search(r'_field(?:_[0-9]+)?_([0-9]+)', field_identifier)
+ if m:
+ numbered_fields[kv_var] = kv_key
+ if kv_key in raw_visible_fields or kv_var in raw_visible_fields:
+ field_numbers[kv_var] = int(m.group(1))
+ m = re.search(r'_field_((?:[0-9]+_)?[0-9]+)', field_identifier)
+ if m:
+ if kv_var not in all_field_numbers:
+ all_field_numbers[kv_var] = set()
+ if '_' in m.group(1):
+ all_field_numbers[kv_var].add(m.group(1))
+ else:
+ all_field_numbers[kv_var].add(int(m.group(1)))
+ except:
+ logmessage("index: error where kv_key is " + str(kv_key) + " and kv_var is " + str(kv_var))
+ list_collect_list = None
+ if not STRICT_MODE:
+ if '_list_collect_list' in post_data:
+ the_list = json.loads(myb64unquote(post_data['_list_collect_list']))
+ if not illegal_variable_name(the_list):
+ list_collect_list = the_list
+ exec(list_collect_list + '._allow_appending()', user_dict)
+ if '_checkboxes' in post_data:
+ checkbox_fields = json.loads(myb64unquote(post_data['_checkboxes'])) # post_data['_checkboxes'].split(",")
+ for checkbox_field, checkbox_value in checkbox_fields.items():
+ if checkbox_field in visible_fields and checkbox_field not in post_data and not (checkbox_field in numbered_fields and numbered_fields[checkbox_field] in post_data):
+ post_data.add(checkbox_field, checkbox_value)
+ if '_empties' in post_data:
+ empty_fields = json.loads(myb64unquote(post_data['_empties']))
+ for empty_field in empty_fields:
+ if empty_field not in post_data:
+ post_data.add(empty_field, 'None')
+ else:
+ empty_fields = {}
+ if '_ml_info' in post_data:
+ ml_info = json.loads(myb64unquote(post_data['_ml_info']))
+ else:
+ ml_info = {}
+ something_changed = False
+ if '_tracker' in post_data and re.search(r'^-?[0-9]+$', post_data['_tracker']) and user_dict['_internal']['tracker'] != int(post_data['_tracker']):
+ if user_dict['_internal']['tracker'] > int(post_data['_tracker']):
+ logmessage("index: the assemble function has been run since the question was posed.")
+ else:
+ logmessage("index: the tracker in the dictionary is behind the tracker in the question.")
+ something_changed = True
+ user_dict['_internal']['tracker'] = max(int(post_data['_tracker']), user_dict['_internal']['tracker'])
+ interview_status.tracker = user_dict['_internal']['tracker']
+ should_assemble = False
+ known_datatypes = {}
+ if not STRICT_MODE:
+ if '_datatypes' in post_data:
+ known_datatypes = json.loads(myb64unquote(post_data['_datatypes']))
+ for data_type in known_datatypes.values():
+ if data_type.startswith('object') or data_type in ('integer', 'float', 'currency', 'number'):
+ should_assemble = True
+ if not should_assemble:
+ for key in post_data:
+ if key.startswith('_') or key in ('csrf_token', 'ajax', 'json', 'informed'):
+ continue
+ try:
+ the_key = from_safeid(key)
+ if the_key.startswith('_field_'):
+ if key in known_varnames:
+ if not (known_varnames[key] in post_data and post_data[known_varnames[key]] != '' and post_data[key] == ''):
+ the_key = from_safeid(known_varnames[key])
+ else:
+ m = re.search(r'^(_field(?:_[0-9]+)?_[0-9]+)(\[.*\])', key)
+ if m:
+ base_orig_key = safeid(m.group(1))
+ if base_orig_key in known_varnames:
+ the_key = myb64unquote(known_varnames[base_orig_key]) + m.group(2)
+ if key_requires_preassembly.search(the_key):
+ if the_key == '_multiple_choice' and '_question_name' in post_data:
+ if refresh_or_continue(interview, post_data):
+ continue
+ should_assemble = True
+ break
+ except BaseException as the_err:
+ logmessage("index: bad key was " + str(key) + " and error was " + the_err.__class__.__name__)
+ try:
+ logmessage("index: bad key error message was " + str(the_err))
+ except:
+ pass
+ if not interview.from_cache and len(interview.mlfields):
+ ensure_training_loaded(interview)
+ debug_mode = interview.debug
+ vars_set = set()
+ old_values = {}
+ new_values = {}
+ no_input_values = {}
+ if ('_email_attachments' in post_data and '_attachment_email_address' in post_data) or '_download_attachments' in post_data:
+ should_assemble = True
+ error_messages = []
+ already_assembled = False
+ if (STRICT_MODE and not disregard_input) or should_assemble or something_changed:
+ with user_dict_context(user_dict):
+ interview.assemble(user_dict, interview_status=interview_status)
+ already_assembled = True
+ if STRICT_MODE and ('_question_name' not in post_data or post_data['_question_name'] != interview_status.question.name):
+ if refresh_or_continue(interview, post_data) is False and action is None and len([key for key in post_data if not (key.startswith('_') or key in ('csrf_token', 'ajax', 'json', 'informed'))]) > 0:
+ error_messages.append(("success", word("Input not processed. Please try again.")))
+ post_data = {}
+ disregard_input = True
+ elif should_assemble and '_question_name' in post_data and post_data['_question_name'] != interview_status.question.name:
+ logmessage("index: not the same question name: " + str(post_data['_question_name']) + " versus " + str(interview_status.question.name))
+ if REQUIRE_IDEMPOTENT:
+ error_messages.append(("success", word("Input not processed because the question changed. Please continue.")))
+ post_data = {}
+ disregard_input = True
+ if STRICT_MODE and not disregard_input:
+ field_info = interview_status.get_field_info()
+ known_datatypes = field_info['datatypes']
+ list_collect_list = field_info['list_collect_list']
+ if list_collect_list is not None:
+ exec(list_collect_list + '._allow_appending()', user_dict)
+ for checkbox_field, checkbox_value in field_info['checkboxes'].items():
+ if checkbox_field in visible_fields and checkbox_field not in post_data and not (checkbox_field in numbered_fields and numbered_fields[checkbox_field] in post_data):
+ for k, v in known_varnames_visible.items():
+ if v == checkbox_field:
+ checkbox_field = k
+ break
+ post_data.add(checkbox_field, checkbox_value)
+ no_input_values[checkbox_field] = checkbox_value
+ empty_fields = field_info['hiddens']
+ for empty_field, data_type in empty_fields.items():
+ if empty_field not in post_data:
+ post_data.add(empty_field, 'None')
+ no_input_values[empty_field] = 'None'
+ ml_info = field_info['ml_info']
+ field_list, list_collect_mappings, iterator_variable = interview_status.get_fields_and_sub_fields_and_collect_fields(user_dict)
+ authorized_fields = [from_safeid(field.saveas) for field in field_list if hasattr(field, 'saveas')]
+ if 'allowed_to_set' in interview_status.extras:
+ authorized_fields.extend(interview_status.extras['allowed_to_set'])
+ if interview_status.question.question_type == "multiple_choice":
+ authorized_fields.append('_multiple_choice')
+ authorized_fields = set(authorized_fields).union(interview_status.get_all_fields_used(user_dict))
+ if interview_status.extras.get('list_collect_is_final', False) and interview_status.extras['list_collect'].auto_gather:
+ if interview_status.extras['list_collect'].ask_number:
+ authorized_fields.add(interview_status.extras['list_collect'].instanceName + ".target_number")
+ else:
+ authorized_fields.add(interview_status.extras['list_collect'].instanceName + ".there_is_another")
+ else:
+ field_list = []
+ list_collect_mappings = {}
+ iterator_variable = None
+ if STRICT_MODE:
+ empty_fields = []
+ authorized_fields = set()
+ changed = False
+ if '_null_question' in post_data or all_invisible:
+ changed = True
+ if '_email_attachments' in post_data and '_attachment_email_address' in post_data:
+ success = False
+ attachment_email_address = post_data['_attachment_email_address'].strip()
+ if '_attachment_include_editable' in post_data:
+ include_editable = bool(post_data['_attachment_include_editable'] == 'True')
+ del post_data['_attachment_include_editable']
+ else:
+ include_editable = False
+ del post_data['_email_attachments']
+ del post_data['_attachment_email_address']
+ if len(interview_status.attachments) > 0:
+ attached_file_count = 0
+ attachment_info = []
+ for the_attachment in interview_status.attachments:
+ file_formats = []
+ if 'pdf' in the_attachment['valid_formats'] or '*' in the_attachment['valid_formats']:
+ file_formats.append('pdf')
+ if include_editable or 'pdf' not in file_formats:
+ if 'rtf' in the_attachment['valid_formats'] or '*' in the_attachment['valid_formats']:
+ file_formats.append('rtf')
+ if 'docx' in the_attachment['valid_formats']:
+ file_formats.append('docx')
+ if 'rtf to docx' in the_attachment['valid_formats']:
+ file_formats.append('rtf to docx')
+ if 'md' in the_attachment['valid_formats']:
+ file_formats.append('md')
+ if 'raw' in the_attachment['valid_formats']:
+ file_formats.append('raw')
+ for file_format in the_attachment.get('manual_formats', []):
+ if file_format not in file_formats:
+ file_formats.append(file_format)
+ for the_format in file_formats:
+ if the_format == 'raw':
+ attachment_info.append({'filename': str(the_attachment['filename']) + the_attachment['raw'], 'number': the_attachment['file'][the_format], 'mimetype': the_attachment['mimetype'][the_format], 'attachment': the_attachment})
+ else:
+ attachment_info.append({'filename': str(the_attachment['filename']) + '.' + str(extension_of_doc_format.get(the_format, the_format)), 'number': the_attachment['file'][the_format], 'mimetype': the_attachment['mimetype'][the_format], 'attachment': the_attachment})
+ attached_file_count += 1
+ worker_key = 'da:worker:uid:' + str(user_code) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)
+ for email_address in re.split(r' *[,;] *', attachment_email_address):
+ try:
+ result = celery_app.signature('tasks.email_attachments', args=[user_code, email_address, attachment_info, get_language()], kwargs={'subject': interview_status.extras.get('email_subject', None), 'body': interview_status.extras.get('email_body', None), 'html': interview_status.extras.get('email_html', None), 'config': interview.consolidated_metadata.get('email config', None)}).delay()
+ r.rpush(worker_key, result.id)
+ success = True
+ except BaseException as errmess:
+ success = False
+ logmessage("index: failed with " + str(errmess))
+ break
+ if success:
+ flash(word("Your documents will be e-mailed to") + " " + str(attachment_email_address) + ".", 'success')
+ else:
+ flash(word("Unable to e-mail your documents to") + " " + str(attachment_email_address) + ".", 'error')
+ else:
+ flash(word("Unable to find documents to e-mail."), 'error')
+ if '_download_attachments' in post_data:
+ success = False
+ if '_attachment_include_editable' in post_data:
+ include_editable = bool(post_data['_attachment_include_editable'] == 'True')
+ del post_data['_attachment_include_editable']
+ else:
+ include_editable = False
+ del post_data['_download_attachments']
+ if len(interview_status.attachments) > 0:
+ attached_file_count = 0
+ files_to_zip = []
+ if 'zip_filename' in interview_status.extras and interview_status.extras['zip_filename']:
+ zip_file_name = interview_status.extras['zip_filename']
+ else:
+ zip_file_name = 'file.zip'
+ for the_attachment in interview_status.attachments:
+ file_formats = []
+ if 'pdf' in the_attachment['valid_formats'] or '*' in the_attachment['valid_formats']:
+ file_formats.append('pdf')
+ if include_editable or 'pdf' not in file_formats:
+ if 'rtf' in the_attachment['valid_formats'] or '*' in the_attachment['valid_formats']:
+ file_formats.append('rtf')
+ if 'docx' in the_attachment['valid_formats']:
+ file_formats.append('docx')
+ if 'rtf to docx' in the_attachment['valid_formats']:
+ file_formats.append('rtf to docx')
+ if 'raw' in the_attachment['valid_formats']:
+ file_formats.append('raw')
+ for file_format in the_attachment.get('manual_formats', []):
+ if file_format not in file_formats:
+ file_formats.append(file_format)
+ for the_format in file_formats:
+ files_to_zip.append(str(the_attachment['file'][the_format]))
+ attached_file_count += 1
+ the_zip_file = zip_file(*files_to_zip, filename=zip_file_name)
+ response = custom_send_file(the_zip_file.path(), mimetype='application/zip', as_attachment=True, download_name=zip_file_name)
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ if response_wrapper:
+ response_wrapper(response)
+ return response
+ if '_the_image' in post_data and (STRICT_MODE is False or interview_status.question.question_type == 'signature'):
+ if STRICT_MODE:
+ file_field = from_safeid(field_info['signature_saveas'])
+ else:
+ file_field = from_safeid(post_data['_save_as'])
+ if illegal_variable_name(file_field):
+ error_messages.append(("error", "Error: Invalid character in file_field: " + str(file_field)))
+ else:
+ if not already_assembled:
+ with user_dict_context(user_dict):
+ interview.assemble(user_dict, interview_status)
+ already_assembled = True
+ initial_string = 'import docassemble.base.util'
+ try:
+ exec(initial_string, user_dict)
+ except BaseException as err_mess:
+ error_messages.append(("error", "Error: " + str(err_mess)))
+ file_field_tr = sub_indices(file_field, user_dict)
+ if '_success' in post_data and post_data['_success']:
+ the_image = base64.b64decode(re.search(r'base64,(.*)', post_data['_the_image']).group(1) + '==')
+ filename = 'canvas.png'
+ file_number = get_new_file_number(user_code, filename, yaml_filename)
+ extension, mimetype = get_ext_and_mimetype(filename)
+ new_file = SavedFile(file_number, extension=extension, fix=True, should_not_exist=True)
+ new_file.write_content(the_image, binary=True)
+ new_file.finalize()
+ the_string = file_field + " = docassemble.base.util.DAFile(" + repr(file_field_tr) + ", filename='" + str(filename) + "', number=" + str(file_number) + ", mimetype='" + str(mimetype) + "', make_pngs=True, extension='" + str(extension) + "')"
+ else:
+ the_string = file_field + " = docassemble.base.util.DAFile(" + repr(file_field_tr) + ")"
+ process_set_variable(file_field, user_dict, vars_set, old_values)
+ try:
+ exec(the_string, user_dict)
+ changed = True
+ except BaseException as err_mess:
+ try:
+ logmessage(err_mess.__class__.__name__ + ": " + str(err_mess) + " after running " + the_string)
+ except:
+ pass
+ error_messages.append(("error", "Error: " + err_mess.__class__.__name__ + ": " + str(err_mess)))
+ if '_next_action_to_set' in post_data:
+ next_action_to_set = json.loads(myb64unquote(post_data['_next_action_to_set']))
+ else:
+ next_action_to_set = None
+ if '_question_name' in post_data and post_data['_question_name'] in interview.questions_by_name:
+ if already_assembled:
+ the_question = interview_status.question
+ else:
+ the_question = interview.questions_by_name[post_data['_question_name']]
+ if not already_assembled:
+ uses_permissions = False
+ for the_field in the_question.fields:
+ if hasattr(the_field, 'permissions'):
+ uses_permissions = True
+ if uses_permissions or the_question.validation_code is not None:
+ with user_dict_context(user_dict):
+ interview.assemble(user_dict, interview_status)
+ else:
+ for the_field in the_question.fields:
+ if hasattr(the_field, 'validate'):
+ with user_dict_context(user_dict):
+ interview.assemble(user_dict, interview_status)
+ break
+ elif already_assembled:
+ the_question = interview_status.question
+ else:
+ the_question = None
+ key_to_orig_key = {}
+ for orig_key in copy.deepcopy(post_data):
+ if orig_key in ('_checkboxes', '_empties', '_ml_info', '_back_one', '_files', '_files_inline', '_question_name', '_the_image', '_save_as', '_success', '_datatypes', '_event', '_visible', '_tracker', '_track_location', '_varnames', '_next_action', '_next_action_to_set', 'ajax', 'json', 'informed', 'csrf_token', '_action', '_readonly', '_order_changes', '_collect', '_collect_delete', '_list_collect_list', '_null_question') or orig_key.startswith('_ignore'):
+ continue
+ try:
+ key = myb64unquote(orig_key)
+ except:
+ continue
+ if key.startswith('_field_'):
+ if orig_key in known_varnames:
+ if not (known_varnames[orig_key] in post_data and post_data[known_varnames[orig_key]] != '' and post_data[orig_key] == ''):
+ post_data[known_varnames[orig_key]] = post_data[orig_key]
+ key_to_orig_key[from_safeid(known_varnames[orig_key])] = orig_key
+ else:
+ m = re.search(r'^(_field(?:_[0-9]+)?_[0-9]+)(\[.*\])', key)
+ if m:
+ base_orig_key = safeid(m.group(1))
+ if base_orig_key in known_varnames:
+ the_key = myb64unquote(known_varnames[base_orig_key]) + m.group(2)
+ key_to_orig_key[the_key] = orig_key
+ full_key = safeid(the_key)
+ post_data[full_key] = post_data[orig_key]
+ if key.endswith('.gathered'):
+ if STRICT_MODE and key not in authorized_fields:
+ raise DAError("The variable " + repr(key) + " was not in the allowed fields, which were " + repr(authorized_fields))
+ objname = re.sub(r'\.gathered$', '', key)
+ if illegal_variable_name(objname):
+ error_messages.append(("error", "Error: Invalid key " + objname))
+ break
+ try:
+ eval(objname, user_dict)
+ except:
+ objname_tr = sub_indices(objname, user_dict)
+ safe_objname = safeid(objname)
+ if safe_objname in known_datatypes:
+ if known_datatypes[safe_objname] in ('object_multiselect', 'object_checkboxes'):
+ ensure_object_exists(objname_tr, 'object_checkboxes', user_dict)
+ elif known_datatypes[safe_objname] in ('multiselect', 'checkboxes'):
+ ensure_object_exists(objname_tr, known_datatypes[safe_objname], user_dict)
+ field_error = {}
+ validated = True
+ pre_user_dict = user_dict
+ imported_core = False
+ special_question = None
+ for orig_key in post_data:
+ if orig_key in ('_checkboxes', '_empties', '_ml_info', '_back_one', '_files', '_files_inline', '_question_name', '_the_image', '_save_as', '_success', '_datatypes', '_event', '_visible', '_tracker', '_track_location', '_varnames', '_next_action', '_next_action_to_set', 'ajax', 'json', 'informed', 'csrf_token', '_action', '_readonly', '_order_changes', '', '_collect', '_collect_delete', '_list_collect_list', '_null_question') or orig_key.startswith('_ignore'):
+ continue
+ raw_data = post_data[orig_key]
+ try:
+ key = myb64unquote(orig_key)
+ except Exception as err:
+ raise DAError("index: invalid name " + str(orig_key)) from err
+ if key.startswith('_field_'):
+ continue
+ bracket_expression = None
+ if orig_key in empty_fields:
+ set_to_empty = empty_fields[orig_key]
+ else:
+ set_to_empty = None
+ if match_brackets.search(key):
+ match = match_inside_and_outside_brackets.search(key)
+ try:
+ key = match.group(1)
+ except Exception as err:
+ try:
+ error_message = "index: invalid bracket name " + str(match.group(1)) + " in " + repr(key)
+ except:
+ error_message = "index: invalid bracket name in " + repr(key)
+ raise DAError(error_message) from err
+ real_key = safeid(key)
+ b_match = match_inside_brackets.search(match.group(2))
+ if b_match:
+ if b_match.group(1) in ('B', 'R', 'O'):
+ try:
+ bracket_expression = from_safeid(b_match.group(2))
+ except:
+ bracket_expression = b_match.group(2)
+ else:
+ bracket_expression = b_match.group(2)
+ bracket = match_inside_brackets.sub(process_bracket_expression, match.group(2))
+ parse_result = parse_var_name(key)
+ if not parse_result['valid']:
+ error_messages.append(("error", "Error: Invalid key " + key + ": " + parse_result['reason']))
+ break
+ pre_bracket_key = key
+ key = key + bracket
+ core_key_name = parse_result['final_parts'][0]
+ whole_key = core_key_name + parse_result['final_parts'][1]
+ real_key = safeid(whole_key)
+ if STRICT_MODE and (pre_bracket_key not in authorized_fields or pre_bracket_key + '.gathered' not in authorized_fields) and (key not in authorized_fields):
+ raise DAError("The variables " + repr(pre_bracket_key) + " and " + repr(key) + " were not in the allowed fields, which were " + repr(authorized_fields))
+ if illegal_variable_name(whole_key) or illegal_variable_name(core_key_name) or illegal_variable_name(key):
+ error_messages.append(("error", "Error: Invalid key " + whole_key))
+ break
+ if whole_key in user_dict:
+ it_exists = True
+ else:
+ try:
+ the_object = eval(whole_key, user_dict) # noqa: F841 # pylint: disable=unused-variable
+ it_exists = True
+ except:
+ it_exists = False
+ if not it_exists:
+ method = None
+ commands = []
+ if parse_result['final_parts'][1] != '':
+ if parse_result['final_parts'][1][0] == '.':
+ try:
+ core_key = eval(core_key_name, user_dict)
+ if hasattr(core_key, 'instanceName'):
+ method = 'attribute'
+ except:
+ pass
+ elif parse_result['final_parts'][1][0] == '[':
+ try:
+ core_key = eval(core_key_name, user_dict)
+ if hasattr(core_key, 'instanceName'):
+ method = 'index'
+ except:
+ pass
+ datatype = known_datatypes.get(real_key, None)
+ if not imported_core:
+ commands.append("import docassemble.base.util")
+ imported_core = True
+ if method == 'attribute':
+ attribute_name = parse_result['final_parts'][1][1:]
+ if datatype in ('multiselect', 'checkboxes'):
+ commands.append(core_key_name + ".initializeAttribute(" + repr(attribute_name) + ", docassemble.base.util.DADict, auto_gather=False, gathered=True)")
+ elif datatype in ('object_multiselect', 'object_checkboxes'):
+ commands.append(core_key_name + ".initializeAttribute(" + repr(attribute_name) + ", docassemble.base.util.DAList, auto_gather=False, gathered=True)")
+ process_set_variable(core_key_name + '.' + attribute_name, user_dict, vars_set, old_values)
+ elif method == 'index':
+ index_name = parse_result['final_parts'][1][1:-1]
+ orig_index_name = index_name
+ if index_name in ('i', 'j', 'k', 'l', 'm', 'n'):
+ index_name = repr(user_dict.get(index_name, index_name))
+ if datatype in ('multiselect', 'checkboxes'):
+ commands.append(core_key_name + ".initializeObject(" + index_name + ", docassemble.base.util.DADict, auto_gather=False, gathered=True)")
+ elif datatype in ('object_multiselect', 'object_checkboxes'):
+ commands.append(core_key_name + ".initializeObject(" + index_name + ", docassemble.base.util.DAList, auto_gather=False, gathered=True)")
+ process_set_variable(core_key_name + '[' + orig_index_name + ']', user_dict, vars_set, old_values)
+ else:
+ whole_key_tr = sub_indices(whole_key, user_dict)
+ if datatype in ('multiselect', 'checkboxes'):
+ commands.append(whole_key + ' = docassemble.base.util.DADict(' + repr(whole_key_tr) + ', auto_gather=False, gathered=True)')
+ elif datatype in ('object_multiselect', 'object_checkboxes'):
+ commands.append(whole_key + ' = docassemble.base.util.DAList(' + repr(whole_key_tr) + ', auto_gather=False, gathered=True)')
+ process_set_variable(whole_key, user_dict, vars_set, old_values)
+ for command in commands:
+ exec(command, user_dict)
+ else:
+ real_key = orig_key
+ parse_result = parse_var_name(key)
+ if not parse_result['valid']:
+ error_messages.append(("error", "Error: Invalid character in key: " + key))
+ break
+ if STRICT_MODE and key not in authorized_fields:
+ raise DAError("The variable " + repr(key) + " was not in the allowed fields, which were " + repr(authorized_fields))
+ if illegal_variable_name(key):
+ error_messages.append(("error", "Error: Invalid key " + key))
+ break
+ do_append = False
+ do_opposite = False
+ is_ml = False
+ is_date = False
+ is_object = False
+ test_data = raw_data
+ if real_key in known_datatypes:
+ if known_datatypes[real_key] in ('boolean', 'multiselect', 'checkboxes'):
+ if raw_data == "True":
+ data = "True"
+ test_data = True
+ elif raw_data == "False":
+ data = "False"
+ test_data = False
+ else:
+ data = "None"
+ test_data = None
+ elif known_datatypes[real_key] == 'threestate':
+ if raw_data == "True":
+ data = "True"
+ test_data = True
+ elif raw_data == "False":
+ data = "False"
+ test_data = False
+ else:
+ data = "None"
+ test_data = None
+ elif known_datatypes[real_key] in ('date', 'datetime', 'datetime-local'):
+ if isinstance(raw_data, str):
+ raw_data = raw_data.strip()
+ if raw_data != '':
+ try:
+ dateutil.parser.parse(raw_data)
+ except:
+ validated = False
+ if known_datatypes[real_key] == 'date':
+ field_error[orig_key] = word("You need to enter a valid date.")
+ else:
+ field_error[orig_key] = word("You need to enter a valid date and time.")
+ new_values[key] = repr(raw_data)
+ continue
+ test_data = raw_data
+ is_date = True
+ data = 'docassemble.base.util.as_datetime(' + repr(raw_data) + ')'
+ else:
+ data = repr('')
+ else:
+ data = repr('')
+ elif known_datatypes[real_key] == 'time':
+ if isinstance(raw_data, str):
+ raw_data = raw_data.strip()
+ if raw_data != '':
+ try:
+ dateutil.parser.parse(raw_data)
+ except:
+ validated = False
+ field_error[orig_key] = word("You need to enter a valid time.")
+ new_values[key] = repr(raw_data)
+ continue
+ test_data = raw_data
+ is_date = True
+ data = 'docassemble.base.util.as_datetime(' + repr(raw_data) + ').time()'
+ else:
+ data = repr('')
+ else:
+ data = repr('')
+ elif known_datatypes[real_key] == 'integer':
+ raw_data = raw_data.replace(',', '')
+ if raw_data.strip() in ('', 'None'):
+ raw_data = '0'
+ try:
+ test_data = int(raw_data)
+ except:
+ validated = False
+ field_error[orig_key] = word("You need to enter a valid number.")
+ new_values[key] = repr(raw_data)
+ continue
+ data = "int(" + repr(raw_data) + ")"
+ elif known_datatypes[real_key] in ('ml', 'mlarea'):
+ is_ml = True
+ data = "None"
+ elif known_datatypes[real_key] in ('number', 'float', 'currency', 'range'):
+ raw_data = raw_data.replace('%', '')
+ raw_data = raw_data.replace(',', '')
+ if raw_data in ('', 'None'):
+ raw_data = 0.0
+ try:
+ test_data = float(raw_data)
+ except:
+ validated = False
+ field_error[orig_key] = word("You need to enter a valid number.")
+ new_values[key] = repr(raw_data)
+ continue
+ data = "float(" + repr(raw_data) + ")"
+ elif known_datatypes[real_key] in ('object', 'object_radio'):
+ if raw_data == '' or set_to_empty:
+ continue
+ if raw_data == 'None':
+ data = 'None'
+ else:
+ data = "_internal['objselections'][" + repr(key) + "][" + repr(raw_data) + "]"
+ elif known_datatypes[real_key] in ('object_multiselect', 'object_checkboxes') and bracket_expression is not None:
+ if raw_data not in ('True', 'False', 'None') or set_to_empty:
+ continue
+ do_append = True
+ if raw_data == 'False':
+ do_opposite = True
+ data = "_internal['objselections'][" + repr(from_safeid(real_key)) + "][" + repr(bracket_expression) + "]"
+ elif set_to_empty in ('object_multiselect', 'object_checkboxes'):
+ continue
+ elif known_datatypes[real_key] in ('file', 'files', 'camera', 'user', 'environment'):
+ continue
+ elif known_datatypes[real_key] in custom_types:
+ info = custom_types[known_datatypes[real_key]]
+ if info['is_object']:
+ is_object = True
+ if set_to_empty:
+ if info['skip_if_empty']:
+ continue
+ test_data = info['class'].empty()
+ if is_object:
+ user_dict['__DANEWOBJECT'] = raw_data
+ data = '__DANEWOBJECT'
+ else:
+ data = repr(test_data)
+ else:
+ key_with_sub = sub_indices(key, user_dict)
+ field_data = {}
+ for field in field_list:
+ if getattr(field, 'saveas', None) == orig_key:
+ for parameter in ('min', 'max', 'minlength', 'maxlength', 'step', 'scale', 'currency symbol', 'field metadata'):
+ if parameter in interview_status.extras and field.number in interview_status.extras[parameter]:
+ field_data[parameter] = interview_status.extras[parameter][field.number]
+ if hasattr(field, 'extras') and 'custom_parameters' in field.extras:
+ for parameter, parameter_value in field.extras['custom_parameters'].items():
+ field_data[parameter] = parameter_value
+ for param_type in ('custom_parameters_code', 'custom_parameters_mako'):
+ if param_type in interview_status.extras and field.number in interview_status.extras[param_type]:
+ for parameter, parameter_value in interview_status.extras[param_type][field.number].items():
+ field_data[parameter] = parameter_value
+ try:
+ if not info['class'].call_validate(raw_data, key_with_sub, field_data):
+ raise DAValidationError(word("You need to enter a valid value."))
+ new_values[key] = repr(raw_data)
+ except DAValidationError as err:
+ validated = False
+ if key in key_to_orig_key:
+ field_error[key_to_orig_key[key]] = word(str(err))
+ else:
+ field_error[orig_key] = word(str(err))
+ new_values[key] = repr(raw_data)
+ continue
+ test_data = info['class'].call_transform(raw_data, key_with_sub, field_data)
+ if is_object:
+ user_dict['__DANEWOBJECT'] = test_data
+ data = '__DANEWOBJECT'
+ else:
+ data = repr(test_data)
+ elif known_datatypes[real_key] == 'raw':
+ if raw_data == "None" and (set_to_empty is not None or (orig_key in no_input_values and no_input_values[orig_key] == 'None')):
+ test_data = None
+ data = "None"
+ else:
+ test_data = raw_data
+ data = repr(raw_data)
+ else:
+ if isinstance(raw_data, str):
+ raw_data = BeautifulSoup(raw_data, "html.parser").get_text('\n')
+ raw_data = re.sub(r'\\', '', raw_data)
+ if raw_data == "None" and (set_to_empty is not None or (orig_key in no_input_values and no_input_values[orig_key] == 'None')):
+ test_data = None
+ data = "None"
+ else:
+ test_data = raw_data
+ data = repr(raw_data)
+ if known_datatypes[real_key] in ('object_multiselect', 'object_checkboxes'):
+ do_append = True
+ elif orig_key in known_datatypes:
+ if known_datatypes[orig_key] in ('boolean', 'multiselect', 'checkboxes'):
+ if raw_data == "True":
+ data = "True"
+ test_data = True
+ elif raw_data == "False":
+ data = "False"
+ test_data = False
+ else:
+ data = "None"
+ test_data = None
+ elif known_datatypes[orig_key] == 'threestate':
+ if raw_data == "True":
+ data = "True"
+ test_data = True
+ elif raw_data == "False":
+ data = "False"
+ test_data = False
+ else:
+ data = "None"
+ test_data = None
+ elif known_datatypes[orig_key] in ('date', 'datetime'):
+ if isinstance(raw_data, str):
+ raw_data = raw_data.strip()
+ if raw_data != '':
+ try:
+ dateutil.parser.parse(raw_data)
+ except:
+ validated = False
+ if known_datatypes[orig_key] == 'date':
+ field_error[orig_key] = word("You need to enter a valid date.")
+ else:
+ field_error[orig_key] = word("You need to enter a valid date and time.")
+ new_values[key] = repr(raw_data)
+ continue
+ test_data = raw_data
+ is_date = True
+ data = 'docassemble.base.util.as_datetime(' + repr(raw_data) + ')'
+ else:
+ data = repr('')
+ else:
+ data = repr('')
+ elif known_datatypes[orig_key] == 'time':
+ if isinstance(raw_data, str):
+ raw_data = raw_data.strip()
+ if raw_data != '':
+ try:
+ dateutil.parser.parse(raw_data)
+ except:
+ validated = False
+ field_error[orig_key] = word("You need to enter a valid time.")
+ new_values[key] = repr(raw_data)
+ continue
+ test_data = raw_data
+ is_date = True
+ data = 'docassemble.base.util.as_datetime(' + repr(raw_data) + ').time()'
+ else:
+ data = repr('')
+ else:
+ data = repr('')
+ elif known_datatypes[orig_key] == 'integer':
+ raw_data = raw_data.replace(',', '')
+ if raw_data.strip() in ('', 'None'):
+ raw_data = '0'
+ try:
+ test_data = int(raw_data)
+ except:
+ validated = False
+ field_error[orig_key] = word("You need to enter a valid number.")
+ new_values[key] = repr(raw_data)
+ continue
+ data = "int(" + repr(raw_data) + ")"
+ elif known_datatypes[orig_key] in ('ml', 'mlarea'):
+ is_ml = True
+ data = "None"
+ elif known_datatypes[orig_key] in ('number', 'float', 'currency', 'range'):
+ raw_data = raw_data.replace(',', '')
+ raw_data = raw_data.replace('%', '')
+ if raw_data in ('', 'None'):
+ raw_data = '0.0'
+ test_data = float(raw_data)
+ data = "float(" + repr(raw_data) + ")"
+ elif known_datatypes[orig_key] in ('object', 'object_radio'):
+ if raw_data == '' or set_to_empty:
+ continue
+ if raw_data == 'None':
+ data = 'None'
+ else:
+ data = "_internal['objselections'][" + repr(key) + "][" + repr(raw_data) + "]"
+ elif set_to_empty in ('object_multiselect', 'object_checkboxes'):
+ continue
+ elif real_key in known_datatypes and known_datatypes[real_key] in ('file', 'files', 'camera', 'user', 'environment'):
+ continue
+ elif known_datatypes[orig_key] in custom_types:
+ info = custom_types[known_datatypes[orig_key]]
+ if info['is_object']:
+ is_object = True
+ if set_to_empty:
+ if info['skip_if_empty']:
+ continue
+ test_data = info['class'].empty()
+ if is_object:
+ user_dict['__DANEWOBJECT'] = raw_data
+ data = '__DANEWOBJECT'
+ else:
+ data = repr(test_data)
+ else:
+ key_tr = sub_indices(key, user_dict)
+ try:
+ if not info['class'].call_validate(raw_data, key_tr):
+ raise DAValidationError(word("You need to enter a valid value."))
+ new_values[key] = repr(raw_data)
+ except DAValidationError as err:
+ validated = False
+ if key in key_to_orig_key:
+ field_error[key_to_orig_key[key]] = word(str(err))
+ else:
+ field_error[orig_key] = word(str(err))
+ new_values[key] = repr(raw_data)
+ continue
+ test_data = info['class'].call_transform(raw_data, key_tr)
+ if is_object:
+ user_dict['__DANEWOBJECT'] = test_data
+ data = '__DANEWOBJECT'
+ else:
+ data = repr(test_data)
+ elif known_datatypes[orig_key] == 'raw':
+ if raw_data == "None" and (set_to_empty is not None or (orig_key in no_input_values and no_input_values[orig_key] == 'None')):
+ test_data = None
+ data = "None"
+ else:
+ test_data = raw_data
+ data = repr(raw_data)
+ else:
+ if isinstance(raw_data, str):
+ raw_data = BeautifulSoup(raw_data.strip(), "html.parser").get_text('\n')
+ raw_data = re.sub(r'\\', '', raw_data)
+ if raw_data == "None" and (set_to_empty is not None or (orig_key in no_input_values and no_input_values[orig_key] == 'None')):
+ test_data = None
+ data = "None"
+ else:
+ test_data = raw_data
+ data = repr(raw_data)
+ elif key == "_multiple_choice":
+ data = "int(" + repr(raw_data) + ")"
+ else:
+ data = repr(raw_data)
+ if key == "_multiple_choice":
+ if '_question_name' in post_data:
+ question_name = post_data['_question_name']
+ if question_name == 'Question_Temp':
+ key = '_internal["answers"][' + repr(interview_status.question.extended_question_name(user_dict)) + ']'
+ else:
+ key = '_internal["answers"][' + repr(interview.questions_by_name[question_name].extended_question_name(user_dict)) + ']'
+ if is_integer.match(str(post_data[orig_key])):
+ the_choice = int(str(post_data[orig_key]))
+ if len(interview.questions_by_name[question_name].fields[0].choices) > the_choice and 'key' in interview.questions_by_name[question_name].fields[0].choices[the_choice] and hasattr(interview.questions_by_name[question_name].fields[0].choices[the_choice]['key'], 'question_type'):
+ if interview.questions_by_name[question_name].fields[0].choices[the_choice]['key'].question_type in ('restart', 'exit', 'logout', 'exit_logout', 'leave'):
+ special_question = interview.questions_by_name[question_name].fields[0].choices[the_choice]['key']
+ elif interview.questions_by_name[question_name].fields[0].choices[the_choice]['key'].question_type == 'continue' and 'continue button field' in interview.questions_by_name[question_name].fields[0].extras:
+ key = interview.questions_by_name[question_name].fields[0].extras['continue button field']
+ data = 'True'
+ if is_date:
+ try:
+ exec("import docassemble.base.util", user_dict)
+ except BaseException as err_mess:
+ error_messages.append(("error", "Error: " + str(err_mess)))
+ key_tr = sub_indices(key, user_dict)
+ if is_ml:
+ try:
+ exec("import docassemble.base.util", user_dict)
+ except BaseException as err_mess:
+ error_messages.append(("error", "Error: " + str(err_mess)))
+ if orig_key in ml_info and 'train' in ml_info[orig_key]:
+ if not ml_info[orig_key]['train']:
+ use_for_training = 'False'
+ else:
+ use_for_training = 'True'
+ else:
+ use_for_training = 'True'
+ if orig_key in ml_info and 'group_id' in ml_info[orig_key]:
+ data = 'docassemble.base.util.DAModel(' + repr(key_tr) + ', group_id=' + repr(ml_info[orig_key]['group_id']) + ', text=' + repr(raw_data) + ', store=' + repr(interview.get_ml_store()) + ', use_for_training=' + use_for_training + ')'
+ else:
+ data = 'docassemble.base.util.DAModel(' + repr(key_tr) + ', text=' + repr(raw_data) + ', store=' + repr(interview.get_ml_store()) + ', use_for_training=' + use_for_training + ')'
+ if set_to_empty:
+ if set_to_empty in ('multiselect', 'checkboxes'):
+ try:
+ exec("import docassemble.base.util", user_dict)
+ except BaseException as err_mess:
+ error_messages.append(("error", "Error: " + str(err_mess)))
+ data = 'docassemble.base.util.DADict(' + repr(key_tr) + ', auto_gather=False, gathered=True)'
+ else:
+ data = 'None'
+ if do_append and not set_to_empty:
+ key_to_use = from_safeid(real_key)
+ if illegal_variable_name(data):
+ logmessage("Received illegal variable name " + str(data))
+ continue
+ if illegal_variable_name(key_to_use):
+ logmessage("Received illegal variable name " + str(key_to_use))
+ continue
+ if do_opposite:
+ the_string = 'if ' + data + ' in ' + key_to_use + '.elements:\n ' + key_to_use + '.remove(' + data + ')'
+ else:
+ the_string = 'if ' + data + ' not in ' + key_to_use + '.elements:\n ' + key_to_use + '.append(' + data + ')'
+ if key_to_use not in new_values:
+ new_values[key_to_use] = []
+ new_values[key_to_use].append(data)
+ else:
+ process_set_variable(key, user_dict, vars_set, old_values)
+ the_string = key + ' = ' + data
+ new_values[key] = data
+ if orig_key in field_numbers and the_question is not None and len(the_question.fields) > field_numbers[orig_key] and hasattr(the_question.fields[field_numbers[orig_key]], 'validate'):
+ field_name = safeid('_field_' + str(field_numbers[orig_key]))
+ if field_name in post_data:
+ the_key = field_name
+ else:
+ the_key = orig_key
+ with user_dict_context(user_dict):
+ the_func = eval(the_question.fields[field_numbers[orig_key]].validate['compute'], user_dict)
+ try:
+ the_result = the_func(test_data)
+ if not the_result:
+ field_error[the_key] = word("Please enter a valid value.")
+ validated = False
+ continue
+ except BaseException as errstr:
+ field_error[the_key] = str(errstr)
+ validated = False
+ continue
+ try:
+ exec(the_string, user_dict)
+ changed = True
+ except BaseException as err_mess:
+ error_messages.append(("error", "Error: " + err_mess.__class__.__name__ + ": " + str(err_mess)))
+ try:
+ logmessage("Tried to run " + the_string + " and got error " + err_mess.__class__.__name__ + ": " + str(err_mess))
+ except:
+ pass
+ if key not in key_to_orig_key:
+ key_to_orig_key[key] = orig_key
+ if validated and special_question is None and not disregard_input:
+ for orig_key in empty_fields:
+ key = myb64unquote(orig_key)
+ if STRICT_MODE and key not in authorized_fields:
+ raise DAError("The variable " + repr(key) + " was not in the allowed fields, which were " + repr(authorized_fields))
+ process_set_variable(key + '.gathered', user_dict, vars_set, old_values)
+ if illegal_variable_name(key):
+ logmessage("Received illegal variable name " + str(key))
+ continue
+ if empty_fields[orig_key] in ('object_multiselect', 'object_checkboxes'):
+ ensure_object_exists(sub_indices(key, user_dict), empty_fields[orig_key], user_dict)
+ exec(key + '.clear()', user_dict)
+ exec(key + '.gathered = True', user_dict)
+ elif empty_fields[orig_key] in ('object', 'object_radio'):
+ process_set_variable(key, user_dict, vars_set, old_values)
+ try:
+ eval(key, user_dict)
+ except:
+ exec(key + ' = None', user_dict)
+ new_values[key] = 'None'
+ if validated and special_question is None:
+ if '_order_changes' in post_data:
+ order_changes = json.loads(post_data['_order_changes'])
+ for table_name, changes in order_changes.items():
+ table_name = myb64unquote(table_name)
+ # if STRICT_MODE and table_name not in authorized_fields:
+ # raise DAError("The variable " + repr(table_name) + " was not in the allowed fields, which were " + repr(authorized_fields))
+ if illegal_variable_name(table_name):
+ error_messages.append(("error", "Error: Invalid character in table reorder: " + str(table_name)))
+ continue
+ try:
+ the_table_list = eval(table_name, user_dict)
+ assert isinstance(the_table_list, DAList)
+ except:
+ error_messages.append(("error", "Error: Invalid table: " + str(table_name)))
+ continue
+ for item in changes:
+ if not (isinstance(item, list) and len(item) == 2 and isinstance(item[0], int) and isinstance(item[1], int)):
+ error_messages.append(("error", "Error: Invalid row number in table reorder: " + str(table_name) + " " + str(item)))
+ break
+ exec(table_name + '._reorder(' + ', '.join([repr(item) for item in changes]) + ')', user_dict)
+ inline_files_processed = []
+ if '_files_inline' in post_data:
+ file_dict = json.loads(myb64unquote(post_data['_files_inline']))
+ if not isinstance(file_dict, dict):
+ raise DAError("inline files was not a dict")
+ file_fields = file_dict['keys']
+ has_invalid_fields = False
+ should_assemble_now = False
+ empty_file_vars = set()
+ for orig_file_field in file_fields:
+ if orig_file_field in known_varnames:
+ orig_file_field = known_varnames[orig_file_field]
+ if orig_file_field not in visible_fields:
+ empty_file_vars.add(orig_file_field)
+ try:
+ file_field = from_safeid(orig_file_field)
+ except:
+ error_messages.append(("error", "Error: Invalid file_field: " + orig_file_field))
+ break
+ if STRICT_MODE and file_field not in authorized_fields:
+ raise DAError("The variable " + repr(file_field) + " was not in the allowed fields, which were " + repr(authorized_fields))
+ if illegal_variable_name(file_field):
+ has_invalid_fields = True
+ error_messages.append(("error", "Error: Invalid character in file_field: " + str(file_field)))
+ break
+ if key_requires_preassembly.search(file_field):
+ should_assemble_now = True
+ if not has_invalid_fields:
+ initial_string = 'import docassemble.base.util'
+ try:
+ exec(initial_string, user_dict)
+ except BaseException as err_mess:
+ error_messages.append(("error", "Error: " + str(err_mess)))
+ if should_assemble_now and not already_assembled:
+ with user_dict_context(user_dict):
+ interview.assemble(user_dict, interview_status)
+ already_assembled = True
+ for orig_file_field_raw in file_fields:
+ if orig_file_field_raw in known_varnames:
+ orig_file_field_raw = known_varnames[orig_file_field_raw]
+ # set_empty = bool(orig_file_field_raw not in visible_fields)
+ if not validated:
+ break
+ orig_file_field = orig_file_field_raw
+ var_to_store = orig_file_field_raw
+ if orig_file_field not in file_dict['values'] and len(known_varnames):
+ for key, val in known_varnames_visible.items():
+ if val == orig_file_field_raw:
+ orig_file_field = key
+ var_to_store = val
+ break
+ if orig_file_field in file_dict['values']:
+ the_files = file_dict['values'][orig_file_field]
+ if the_files:
+ files_to_process = []
+ for the_file in the_files:
+ temp_file = tempfile.NamedTemporaryFile(prefix="datemp", delete=False)
+ start_index = 0
+ char_index = 0
+ for char in the_file['content']:
+ char_index += 1
+ if char == ',':
+ start_index = char_index
+ break
+ temp_file.write(codecs.decode(bytearray(the_file['content'][start_index:], encoding='utf-8'), 'base64'))
+ temp_file.close()
+ safe_filename = secure_filename(the_file['name'])
+ filename = secure_filename_unicode_ok(the_file['name'])
+ extension, mimetype = get_ext_and_mimetype(filename)
+ try:
+ img = Image.open(temp_file.name)
+ the_format = img.format.lower()
+ the_format = re.sub(r'jpeg', 'jpg', the_format)
+ except:
+ the_format = extension
+ logmessage("Could not read file type from file " + str(filename))
+ if the_format != extension:
+ filename = re.sub(r'\.[^\.]+$', '', filename) + '.' + the_format
+ extension, mimetype = get_ext_and_mimetype(filename)
+ file_number = get_new_file_number(user_code, safe_filename, yaml_filename)
+ saved_file = SavedFile(file_number, extension=extension, fix=True, should_not_exist=True)
+ process_file(saved_file, temp_file.name, mimetype, extension)
+ files_to_process.append((filename, file_number, mimetype, extension))
+ try:
+ file_field = from_safeid(var_to_store)
+ except:
+ error_messages.append(("error", "Error: Invalid file_field: " + str(var_to_store)))
+ break
+ if STRICT_MODE and file_field not in authorized_fields:
+ raise DAError("The variable " + repr(file_field) + " was not in the allowed fields, which were " + repr(authorized_fields))
+ if illegal_variable_name(file_field):
+ error_messages.append(("error", "Error: Invalid character in file_field: " + str(file_field)))
+ break
+ file_field_tr = sub_indices(file_field, user_dict)
+ if len(files_to_process) > 0:
+ elements = []
+ indexno = 0
+ for (filename, file_number, mimetype, extension) in files_to_process:
+ elements.append("docassemble.base.util.DAFile(" + repr(file_field_tr + "[" + str(indexno) + "]") + ", filename=" + repr(filename) + ", number=" + str(file_number) + ", make_pngs=True, mimetype=" + repr(mimetype) + ", extension=" + repr(extension) + ")")
+ indexno += 1
+ the_file_list = "docassemble.base.util.DAFileList(" + repr(file_field_tr) + ", elements=[" + ", ".join(elements) + "])"
+ if var_to_store in field_numbers and the_question is not None and len(the_question.fields) > field_numbers[var_to_store]:
+ the_field = the_question.fields[field_numbers[var_to_store]]
+ add_permissions_for_field(the_field, interview_status, files_to_process)
+ if hasattr(the_field, 'validate'):
+ the_key = orig_file_field
+ with user_dict_context(user_dict):
+ the_func = eval(the_field.validate['compute'], user_dict)
+ try:
+ the_result = the_func(eval(the_file_list))
+ if not the_result:
+ field_error[the_key] = word("Please enter a valid value.")
+ validated = False
+ break
+ except BaseException as errstr:
+ field_error[the_key] = str(errstr)
+ validated = False
+ break
+ the_string = file_field + " = " + the_file_list
+ inline_files_processed.append(file_field)
+ else:
+ the_string = file_field + " = None"
+ key_to_orig_key[file_field] = orig_file_field
+ process_set_variable(file_field, user_dict, vars_set, old_values)
+ try:
+ exec(the_string, user_dict)
+ changed = True
+ except BaseException as err_mess:
+ try:
+ logmessage("Error: " + err_mess.__class__.__name__ + ": " + str(err_mess) + " after trying to run " + the_string)
+ except:
+ pass
+ error_messages.append(("error", "Error: " + err_mess.__class__.__name__ + ": " + str(err_mess)))
+ else:
+ try:
+ file_field = from_safeid(var_to_store)
+ except:
+ error_messages.append(("error", "Error: Invalid file_field: " + str(var_to_store)))
+ break
+ if STRICT_MODE and file_field not in authorized_fields:
+ raise DAError("The variable " + repr(file_field) + " was not in the allowed fields, which were " + repr(authorized_fields))
+ if illegal_variable_name(file_field):
+ error_messages.append(("error", "Error: Invalid character in file_field: " + str(file_field)))
+ break
+ the_string = file_field + " = None"
+ key_to_orig_key[file_field] = orig_file_field
+ process_set_variable(file_field, user_dict, vars_set, old_values)
+ try:
+ exec(the_string, user_dict)
+ changed = True
+ except BaseException as err_mess:
+ logmessage("Error: " + err_mess.__class__.__name__ + ": " + str(err_mess) + " after running " + the_string)
+ error_messages.append(("error", "Error: " + err_mess.__class__.__name__ + ": " + str(err_mess)))
+ if '_files' in post_data or (STRICT_MODE and (not disregard_input) and len(field_info['files']) > 0):
+ if STRICT_MODE:
+ file_fields = field_info['files']
+ else:
+ file_fields = json.loads(myb64unquote(post_data['_files']))
+ has_invalid_fields = False
+ should_assemble_now = False
+ empty_file_vars = set()
+ for orig_file_field in file_fields:
+ if orig_file_field not in raw_visible_fields:
+ continue
+ file_field_to_use = orig_file_field
+ if file_field_to_use in known_varnames:
+ file_field_to_use = known_varnames[orig_file_field]
+ if file_field_to_use not in visible_fields:
+ empty_file_vars.add(orig_file_field)
+ try:
+ file_field = from_safeid(file_field_to_use)
+ except:
+ error_messages.append(("error", "Error: Invalid file_field: " + str(file_field_to_use)))
+ break
+ if STRICT_MODE and file_field not in authorized_fields:
+ raise DAError("The variable " + repr(file_field) + " was not in the allowed fields, which were " + repr(authorized_fields))
+ if illegal_variable_name(file_field):
+ has_invalid_fields = True
+ error_messages.append(("error", "Error: Invalid character in file_field: " + str(file_field)))
+ break
+ if key_requires_preassembly.search(file_field):
+ should_assemble_now = True
+ key_to_orig_key[file_field] = orig_file_field
+ if not has_invalid_fields:
+ initial_string = 'import docassemble.base.util'
+ try:
+ exec(initial_string, user_dict)
+ except BaseException as err_mess:
+ error_messages.append(("error", "Error: " + str(err_mess)))
+ if not already_assembled:
+ with user_dict_context(user_dict):
+ interview.assemble(user_dict, interview_status)
+ already_assembled = True
+ for orig_file_field_raw in file_fields:
+ if orig_file_field_raw not in raw_visible_fields:
+ continue
+ if orig_file_field_raw in known_varnames:
+ orig_file_field_raw = known_varnames[orig_file_field_raw]
+ if orig_file_field_raw not in visible_fields:
+ continue
+ if not validated:
+ break
+ orig_file_field = orig_file_field_raw
+ var_to_store = orig_file_field_raw
+ if (orig_file_field not in request.files or request.files[orig_file_field].filename == "") and len(known_varnames):
+ for key, val in known_varnames_visible.items():
+ if val == orig_file_field_raw:
+ orig_file_field = key
+ var_to_store = val
+ break
+ if orig_file_field in request.files and request.files[orig_file_field].filename != "":
+ the_files = request.files.getlist(orig_file_field)
+ if the_files:
+ files_to_process = []
+ for the_file in the_files:
+ if is_ajax:
+ return_fake_html = True
+ safe_filename = secure_filename(the_file.filename)
+ filename = secure_filename_unicode_ok(the_file.filename)
+ file_number = get_new_file_number(user_code, safe_filename, yaml_filename)
+ extension, mimetype = get_ext_and_mimetype(filename)
+ saved_file = SavedFile(file_number, extension=extension, fix=True, should_not_exist=True)
+ temp_file = tempfile.NamedTemporaryFile(prefix="datemp", suffix='.' + extension, delete=False)
+ the_file.save(temp_file.name)
+ process_file(saved_file, temp_file.name, mimetype, extension)
+ files_to_process.append((filename, file_number, mimetype, extension))
+ try:
+ file_field = from_safeid(var_to_store)
+ except:
+ error_messages.append(("error", "Error: Invalid file_field: " + str(var_to_store)))
+ break
+ if STRICT_MODE and file_field not in authorized_fields:
+ raise DAError("The variable " + repr(file_field) + " was not in the allowed fields, which were " + repr(authorized_fields))
+ if illegal_variable_name(file_field):
+ error_messages.append(("error", "Error: Invalid character in file_field: " + str(file_field)))
+ break
+ file_field_tr = sub_indices(file_field, user_dict)
+ if len(files_to_process) > 0:
+ elements = []
+ indexno = 0
+ for (filename, file_number, mimetype, extension) in files_to_process:
+ elements.append("docassemble.base.util.DAFile(" + repr(file_field_tr + '[' + str(indexno) + ']') + ", filename=" + repr(filename) + ", number=" + str(file_number) + ", make_pngs=True, mimetype=" + repr(mimetype) + ", extension=" + repr(extension) + ")")
+ indexno += 1
+ the_file_list = "docassemble.base.util.DAFileList(" + repr(file_field_tr) + ", elements=[" + ", ".join(elements) + "])"
+ if var_to_store in field_numbers and the_question is not None and len(the_question.fields) > field_numbers[var_to_store]:
+ the_field = the_question.fields[field_numbers[var_to_store]]
+ add_permissions_for_field(the_field, interview_status, files_to_process)
+ if hasattr(the_question.fields[field_numbers[var_to_store]], 'validate'):
+ the_key = orig_file_field
+ with user_dict_context(user_dict):
+ the_func = eval(the_question.fields[field_numbers[var_to_store]].validate['compute'], user_dict)
+ try:
+ the_result = the_func(eval(the_file_list))
+ if not the_result:
+ field_error[the_key] = word("Please enter a valid value.")
+ validated = False
+ break
+ except BaseException as errstr:
+ field_error[the_key] = str(errstr)
+ validated = False
+ break
+ the_string = file_field + " = " + the_file_list
+ else:
+ the_string = file_field + " = None"
+ process_set_variable(file_field, user_dict, vars_set, old_values)
+ if validated:
+ try:
+ exec(the_string, user_dict)
+ changed = True
+ except BaseException as err_mess:
+ logmessage("Error: " + err_mess.__class__.__name__ + ": " + str(err_mess) + "after running " + the_string)
+ error_messages.append(("error", "Error: " + err_mess.__class__.__name__ + ": " + str(err_mess)))
+ else:
+ try:
+ file_field = from_safeid(var_to_store)
+ except:
+ error_messages.append(("error", "Error: Invalid file_field: " + str(var_to_store)))
+ break
+ if file_field in inline_files_processed:
+ continue
+ if STRICT_MODE and file_field not in authorized_fields:
+ raise DAError("The variable " + repr(file_field) + " was not in the allowed fields, which were " + repr(authorized_fields))
+ if illegal_variable_name(file_field):
+ error_messages.append(("error", "Error: Invalid character in file_field: " + str(file_field)))
+ break
+ the_string = file_field + " = None"
+ process_set_variable(file_field, user_dict, vars_set, old_values)
+ try:
+ exec(the_string, user_dict)
+ changed = True
+ except BaseException as err_mess:
+ logmessage("Error: " + err_mess.__class__.__name__ + ": " + str(err_mess) + "after running " + the_string)
+ error_messages.append(("error", "Error: " + err_mess.__class__.__name__ + ": " + str(err_mess)))
+ if validated:
+ if 'informed' in request.form:
+ user_dict['_internal']['informed'][the_user_id] = {}
+ for key in request.form['informed'].split(','):
+ user_dict['_internal']['informed'][the_user_id][key] = 1
+ if changed and '_question_name' in post_data and post_data['_question_name'] not in user_dict['_internal']['answers']:
+ try:
+ interview.questions_by_name[post_data['_question_name']].mark_as_answered(user_dict)
+ except:
+ logmessage("index: question name could not be found")
+ if ('_event' in post_data or (STRICT_MODE and (not disregard_input) and field_info['orig_sought'] is not None)) and 'event_stack' in user_dict['_internal']:
+ if STRICT_MODE:
+ events_list = [field_info['orig_sought']]
+ else:
+ events_list = json.loads(myb64unquote(post_data['_event']))
+ if len(events_list) > 0:
+ session_uid = interview_status.current_info['user']['session_uid']
+ if session_uid in user_dict['_internal']['event_stack'] and len(user_dict['_internal']['event_stack'][session_uid]):
+ for event_name in events_list:
+ if user_dict['_internal']['event_stack'][session_uid][0]['action'] == event_name:
+ user_dict['_internal']['event_stack'][session_uid].pop(0)
+ if 'action' in interview_status.current_info and interview_status.current_info['action'] == event_name:
+ del interview_status.current_info['action']
+ if 'arguments' in interview_status.current_info:
+ del interview_status.current_info['arguments']
+ break
+ if len(user_dict['_internal']['event_stack'][session_uid]) == 0:
+ break
+ for var_name in list(vars_set):
+ vars_set.add(sub_indices(var_name, user_dict))
+ if len(vars_set) > 0 and 'event_stack' in user_dict['_internal']:
+ session_uid = interview_status.current_info['user']['session_uid']
+ popped = True
+ while popped:
+ popped = False
+ if session_uid in user_dict['_internal']['event_stack'] and len(user_dict['_internal']['event_stack'][session_uid]):
+ for var_name in vars_set:
+ if user_dict['_internal']['event_stack'][session_uid][0]['action'] == var_name:
+ popped = True
+ user_dict['_internal']['event_stack'][session_uid].pop(0)
+ if len(user_dict['_internal']['event_stack'][session_uid]) == 0:
+ break
+ else:
+ steps, user_dict, is_encrypted = fetch_user_dict(user_code, yaml_filename, secret=secret)
+ else:
+ steps, user_dict, is_encrypted = fetch_user_dict(user_code, yaml_filename, secret=secret)
+ if validated and special_question is None:
+ if '_collect_delete' in post_data and list_collect_list is not None:
+ to_delete = json.loads(post_data['_collect_delete'])
+ is_ok = True
+ for item in to_delete:
+ if not isinstance(item, int):
+ is_ok = False
+ if is_ok:
+ exec(list_collect_list + ' ._remove_items_by_number(' + ', '.join(map(str, to_delete)) + ')', user_dict)
+ changed = True
+ if '_collect' in post_data and list_collect_list is not None:
+ collect = json.loads(myb64unquote(post_data['_collect']))
+ if collect['function'] == 'add':
+ add_action_to_stack(interview_status, user_dict, '_da_list_add', {'list': list_collect_list, 'complete': False})
+ if list_collect_list is not None:
+ exec(list_collect_list + '._disallow_appending()', user_dict)
+ if the_question is not None and the_question.validation_code:
+ try:
+ with user_dict_context(user_dict):
+ exec(the_question.validation_code, user_dict)
+ except BaseException as validation_error:
+ the_error_message = str(validation_error)
+ logmessage("index: exception during validation: " + the_error_message)
+ if the_error_message == '':
+ the_error_message = word("Please enter a valid value.")
+ if isinstance(validation_error, DAValidationError) and isinstance(validation_error.field, str): # pylint: disable=no-member
+ 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:
+ if item.startswith(the_field + '['):
+ the_field = item
+ break
+ if the_field in key_to_orig_key:
+ field_error[key_to_orig_key[the_field]] = the_error_message
+ else:
+ error_messages.append(("error", the_error_message))
+ else:
+ error_messages.append(("error", the_error_message))
+ validated = False
+ steps, user_dict, is_encrypted = fetch_user_dict(user_code, yaml_filename, secret=secret)
+ if validated:
+ iterator_backup = {}
+ old_values_backup = {}
+ for var_name in vars_set:
+ with user_dict_context(user_dict):
+ if var_name in interview.invalidation_todo:
+ interview.invalidate_dependencies(var_name, user_dict, old_values)
+ elif var_name in interview.onchange_todo:
+ if not already_assembled:
+ interview.assemble(user_dict, interview_status)
+ already_assembled = True
+ interview.invalidate_dependencies(var_name, user_dict, old_values)
+ try:
+ del user_dict['_internal']['dirty'][var_name]
+ except:
+ pass
+ if iterator_variable is not None and var_name in list_collect_mappings:
+ iterator_value, the_var_name = list_collect_mappings[var_name]
+ with user_dict_context(user_dict):
+ if the_var_name in interview.invalidation_todo:
+ if iterator_variable in user_dict:
+ iterator_backed_up = True
+ iterator_backup = user_dict[iterator_variable]
+ else:
+ iterator_backed_up = False
+ if the_var_name in old_values:
+ old_values_backed_up = True
+ old_values_backup = old_values[the_var_name]
+ else:
+ old_values_backed_up = False
+ old_values[the_var_name] = old_values[var_name]
+ user_dict[iterator_variable] = iterator_value
+ interview.invalidate_dependencies(the_var_name, user_dict, old_values)
+ if iterator_backed_up:
+ user_dict[iterator_variable] = iterator_backup
+ else:
+ del user_dict[iterator_variable]
+ if old_values_backed_up:
+ old_values[the_var_name] = old_values_backup
+ else:
+ del old_values[the_var_name]
+ elif the_var_name in interview.onchange_todo:
+ if not already_assembled:
+ interview.assemble(user_dict, interview_status)
+ already_assembled = True
+ if iterator_variable in user_dict:
+ iterator_backed_up = True
+ iterator_backup = user_dict[iterator_variable]
+ else:
+ iterator_backed_up = False
+ if the_var_name in old_values:
+ old_values_backed_up = True
+ old_values_backup = old_values[the_var_name]
+ else:
+ old_values_backed_up = False
+ old_values[the_var_name] = old_values[var_name]
+ user_dict[iterator_variable] = iterator_value
+ with user_dict_context(user_dict):
+ interview.invalidate_dependencies(the_var_name, user_dict, old_values)
+ if iterator_backed_up:
+ user_dict[iterator_variable] = iterator_backup
+ else:
+ del user_dict[iterator_variable]
+ if old_values_backed_up:
+ old_values[the_var_name] = old_values_backup
+ else:
+ del old_values[the_var_name]
+ try:
+ del user_dict['_internal']['dirty'][the_var_name]
+ except:
+ pass
+ if action is not None:
+ interview_status.current_info.update(action)
+ with user_dict_context(user_dict), old_user_dict_context(old_user_dict):
+ interview.assemble(user_dict, interview_status, old_user_dict, force_question=special_question)
+ current_language = get_language()
+ session['language'] = current_language
+ if not interview_status.can_go_back:
+ user_dict['_internal']['steps_offset'] = steps
+ if was_new:
+ this_thread.misc['save_status'] = SS_OVERWRITE
+ if not changed and url_args_changed:
+ changed = True
+ validated = True
+ if interview_status.question.question_type == "restart":
+ manual_checkout(manual_filename=yaml_filename)
+ url_args = user_dict['url_args']
+ referer = user_dict['_internal'].get('referer', None)
+ user_dict = fresh_dictionary()
+ user_dict['url_args'] = url_args
+ user_dict['_internal']['referer'] = referer
+ the_current_info = current_info(yaml=yaml_filename, req=request, interface=the_interface, session_info=session_info, secret=secret, device_id=device_id)
+ this_thread.current_info = the_current_info
+ interview_status = InterviewStatus(current_info=the_current_info)
+ reset_user_dict(user_code, yaml_filename)
+ if 'visitor_secret' not in request.cookies:
+ save_user_dict_key(user_code, yaml_filename)
+ update_session(yaml_filename, uid=user_code, key_logged=True)
+ steps = 1
+ changed = False
+ action = None
+ with user_dict_context(user_dict):
+ interview.assemble(user_dict, interview_status)
+ elif interview_status.question.question_type == "new_session":
+ manual_checkout(manual_filename=yaml_filename)
+ url_args = user_dict['url_args']
+ referer = user_dict['_internal'].get('referer', None)
+ the_current_info = current_info(yaml=yaml_filename, req=request, interface=the_interface, session_info=session_info, secret=secret, device_id=device_id)
+ this_thread.current_info = the_current_info
+ interview_status = InterviewStatus(current_info=the_current_info)
+ if this_thread.misc.get('save_status', SS_NEW) != SS_IGNORE:
+ release_lock(user_code, yaml_filename)
+ user_code, user_dict = reset_session(yaml_filename, secret)
+ user_dict['url_args'] = url_args
+ user_dict['_internal']['referer'] = referer
+ if 'visitor_secret' not in request.cookies:
+ save_user_dict_key(user_code, yaml_filename)
+ update_session(yaml_filename, uid=user_code, key_logged=True)
+ steps = 1
+ changed = False
+ action = None
+ with user_dict_context(user_dict):
+ interview.assemble(user_dict, interview_status)
+ title_info = interview.get_title(user_dict, status=interview_status, converter=lambda content, part: title_converter(content, part, interview_status))
+ save_status = this_thread.misc.get('save_status', SS_NEW)
+ if interview_status.question.question_type == "interview_exit":
+ exit_link = title_info.get('exit link', 'leave')
+ if exit_link in ('exit', 'leave', 'logout', 'exit_logout'):
+ interview_status.question.question_type = exit_link
+ if interview_status.question.question_type == "exit":
+ manual_checkout(manual_filename=yaml_filename)
+ reset_user_dict(user_code, yaml_filename)
+ delete_session_for_interview(i=yaml_filename)
+ if save_status != SS_IGNORE:
+ release_lock(user_code, yaml_filename)
+ session["_flashes"] = []
+ logmessage("Redirecting because of an exit.")
+ if interview_status.question_text != '':
+ response = do_redirect(interview_status.question_text, is_ajax, is_json, js_target)
+ else:
+ response = do_redirect(title_info.get('exit url', None) or exit_page, is_ajax, is_json, js_target)
+ if return_fake_html:
+ fake_up(response, current_language)
+ if response_wrapper:
+ response_wrapper(response)
+ return response
+ if interview_status.question.question_type in ("exit_logout", "logout"):
+ manual_checkout(manual_filename=yaml_filename)
+ if interview_status.question.question_type == "exit_logout":
+ reset_user_dict(user_code, yaml_filename)
+ if save_status != SS_IGNORE:
+ release_lock(user_code, yaml_filename)
+ delete_session_info()
+ logmessage("Redirecting because of a logout.")
+ if interview_status.question_text != '':
+ response = do_redirect(interview_status.question_text, is_ajax, is_json, js_target)
+ else:
+ response = do_redirect(title_info.get('exit url', None) or exit_page, is_ajax, is_json, js_target)
+ if current_user.is_authenticated:
+ docassemble_flask_user.signals.user_logged_out.send(current_app._get_current_object(), user=current_user)
+ logout_user()
+ delete_session_info()
+ session.clear()
+ response.set_cookie('remember_token', '', expires=0)
+ response.set_cookie('visitor_secret', '', expires=0)
+ response.set_cookie('secret', '', expires=0)
+ response.set_cookie('session', '', expires=0)
+ if return_fake_html:
+ fake_up(response, current_language)
+ return response
+ if interview_status.question.question_type == "refresh":
+ if save_status != SS_IGNORE:
+ release_lock(user_code, yaml_filename)
+ response = do_refresh(is_ajax, yaml_filename)
+ if return_fake_html:
+ fake_up(response, current_language)
+ if response_wrapper:
+ response_wrapper(response)
+ return response
+ if interview_status.question.question_type == "signin":
+ if save_status != SS_IGNORE:
+ release_lock(user_code, yaml_filename)
+ logmessage("Redirecting because of a signin.")
+ response = do_redirect(url_for('user.login', next=url_for('interview.index', i=yaml_filename, session=user_code)), is_ajax, is_json, js_target)
+ if return_fake_html:
+ fake_up(response, current_language)
+ if response_wrapper:
+ response_wrapper(response)
+ return response
+ if interview_status.question.question_type == "register":
+ if save_status != SS_IGNORE:
+ release_lock(user_code, yaml_filename)
+ logmessage("Redirecting because of a register.")
+ response = do_redirect(url_for('user.register', next=url_for('interview.index', i=yaml_filename, session=user_code)), is_ajax, is_json, js_target)
+ if return_fake_html:
+ fake_up(response, current_language)
+ if response_wrapper:
+ response_wrapper(response)
+ return response
+ if interview_status.question.question_type == "leave":
+ if save_status != SS_IGNORE:
+ release_lock(user_code, yaml_filename)
+ session["_flashes"] = []
+ logmessage("Redirecting because of a leave.")
+ if interview_status.question_text != '':
+ response = do_redirect(interview_status.question_text, is_ajax, is_json, js_target)
+ else:
+ response = do_redirect(title_info.get('exit url', None) or exit_page, is_ajax, is_json, js_target)
+ if return_fake_html:
+ fake_up(response, current_language)
+ if response_wrapper:
+ response_wrapper(response)
+ return response
+ if interview.use_progress_bar and interview_status.question.progress is not None:
+ if interview_status.question.progress == -1:
+ user_dict['_internal']['progress'] = None
+ elif user_dict['_internal']['progress'] is None or interview_status.question.interview.options.get('strict progress', False) or interview_status.question.progress > user_dict['_internal']['progress']:
+ user_dict['_internal']['progress'] = interview_status.question.progress
+ if interview.use_navigation and interview_status.question.section is not None and this_thread.current_section:
+ user_dict['nav'].set_section(this_thread.current_section)
+ if interview_status.question.question_type == "wait" and is_ajax:
+ response_to_send = jsonify(action='wait', sleep=interview_status.question.sleep, csrf_token=generate_csrf())
+ elif interview_status.question.question_type == "response":
+ if is_ajax:
+ if save_status != SS_IGNORE:
+ release_lock(user_code, yaml_filename)
+ response = jsonify(action='resubmit', csrf_token=generate_csrf())
+ if return_fake_html:
+ fake_up(response, current_language)
+ if response_wrapper:
+ response_wrapper(response)
+ return response
+ if hasattr(interview_status.question, 'response_code'):
+ resp_code = interview_status.question.response_code
+ else:
+ resp_code = 200
+ if hasattr(interview_status.question, 'all_variables'):
+ if hasattr(interview_status.question, 'include_internal'):
+ include_internal = interview_status.question.include_internal
+ else:
+ include_internal = False
+ response_to_send = make_response(dict_as_json(user_dict, include_internal=include_internal).encode('utf-8'), resp_code)
+ elif hasattr(interview_status.question, 'binaryresponse'):
+ response_to_send = make_response(interview_status.question.binaryresponse, resp_code)
+ else:
+ response_to_send = make_response(interview_status.question_text.encode('utf-8'), resp_code)
+ response_to_send.headers['Content-Type'] = interview_status.extras['content_type']
+ elif interview_status.question.question_type == "sendfile":
+ if is_ajax:
+ if save_status != SS_IGNORE:
+ release_lock(user_code, yaml_filename)
+ response = jsonify(action='resubmit', csrf_token=generate_csrf())
+ if return_fake_html:
+ fake_up(response, current_language)
+ if response_wrapper:
+ response_wrapper(response)
+ return response
+ if interview_status.question.response_file is not None:
+ the_path = interview_status.question.response_file.path()
+ else:
+ logmessage("index: could not send file because the response was None")
+ return ('File not found', 404)
+ if not os.path.isfile(the_path):
+ logmessage("index: could not send file because file (" + the_path + ") not found")
+ return ('File not found', 404)
+ response_to_send = custom_send_file(the_path, mimetype=interview_status.extras['content_type'])
+ response_to_send.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
+ elif interview_status.question.question_type == "redirect":
+ logmessage("Redirecting because of a redirect.")
+ session["_flashes"] = []
+ response_to_send = do_redirect(interview_status.question_text, is_ajax, is_json, js_target)
+ else:
+ response_to_send = None
+ if (not interview_status.followed_mc) and len(user_dict['_internal']['answers']):
+ user_dict['_internal']['answers'].clear()
+ if not validated:
+ changed = False
+ if changed and validated:
+ if save_status == SS_NEW:
+ steps += 1
+ user_dict['_internal']['steps'] = steps
+ if action and not changed:
+ changed = True
+ if save_status == SS_NEW:
+ steps += 1
+ user_dict['_internal']['steps'] = steps
+ if changed and interview.use_progress_bar and interview_status.question.progress is None and save_status == SS_NEW:
+ advance_progress(user_dict, interview)
+ title_info = interview.get_title(user_dict, status=interview_status, converter=lambda content, part: title_converter(content, part, interview_status))
+ # Stash the values of the special_vars now, which is after
+ # .assemble() has been called and before save_user_dict is called,
+ # which would delete the values.
+ interview_status.special_vars = {var_name: user_dict[var_name] for var_name in ('x', 'i', 'j', 'k', 'l', 'm', 'n') if var_name in user_dict}
+ if save_status != SS_IGNORE:
+ if save_status == SS_OVERWRITE:
+ changed = False
+ save_user_dict(user_code, user_dict, yaml_filename, secret=secret, changed=changed, encrypt=encrypted, steps=steps)
+ if user_dict.get('multi_user', False) is True and encrypted is True:
+ encrypted = False
+ update_session(yaml_filename, encrypted=encrypted)
+ decrypt_session(secret, user_code=user_code, filename=yaml_filename)
+ if user_dict.get('multi_user', False) is False and encrypted is False:
+ encrypt_session(secret, user_code=user_code, filename=yaml_filename)
+ encrypted = True
+ update_session(yaml_filename, encrypted=encrypted)
+ if response_to_send is not None:
+ if save_status != SS_IGNORE:
+ release_lock(user_code, yaml_filename)
+ if return_fake_html:
+ fake_up(response_to_send, current_language)
+ if response_wrapper:
+ response_wrapper(response_to_send)
+ return response_to_send
+ messages = get_flashed_messages(with_categories=True) + error_messages
+ if messages and len(messages):
+ notification_interior = ''
+ for classname, message in messages:
+ if classname == 'error':
+ classname = 'danger'
+ notification_interior += NOTIFICATION_MESSAGE % (classname, str(message))
+ flash_content = NOTIFICATION_CONTAINER % (notification_interior,)
+ else:
+ flash_content = ''
+ if 'reload_after' in interview_status.extras:
+ reload_after = 1000 * int(interview_status.extras['reload_after'])
+ else:
+ reload_after = 0
+ allow_going_back = bool(interview_status.extras['can_go_back'] and (steps - user_dict['_internal']['steps_offset']) > 1)
+ if hasattr(interview_status.question, 'id'):
+ question_id = interview_status.question.id
+ else:
+ question_id = None
+ question_id_dict = {'id': question_id}
+ if interview.options.get('analytics on', True):
+ if 'segment' in interview_status.extras:
+ question_id_dict['segment'] = interview_status.extras['segment']
+ if 'ga_id' in interview_status.extras:
+ question_id_dict['ga'] = interview_status.extras['ga_id']
+ append_script_urls = []
+ scripts = ''
+ if interview_status.question.question_type == "signature":
+ if 'pen color' in interview_status.extras and 0 in interview_status.extras['pen color']:
+ pen_color = interview_status.extras['pen color'][0].strip()
+ else:
+ pen_color = '#000'
+ if 0 in interview_status.defaults and isinstance(interview_status.defaults[0], DAFile) and interview_status.defaults[0].ok:
+ try:
+ default_image = f'data:{interview_status.defaults[0].mimetype};base64,{base64.b64encode(interview_status.defaults[0].slurp(auto_decode=False)).decode("utf-8")}'
+ except Exception as err:
+ logmessage("Could not convert signature into a data URL: " + err.__class__.__name__ + ": " + str(err))
+ default_image = None
+ else:
+ default_image = None
+ interview_status.extra_scripts.append({"type": "signature", "color": pen_color, "default": default_image})
+ if not is_ajax:
+ if interview.options.get('analytics on', True):
+ if ga_configured:
+ ga_ids = google_config.get('analytics id')
+ else:
+ ga_ids = None
+ segment_id = daconfig.get('segment id')
+ else:
+ ga_ids = None
+ segment_id = None
+ if is_js:
+ scripts = additional_scripts(ga_ids, as_javascript=True)
+ else:
+ scripts = standard_scripts(interview_language=current_language) + additional_scripts(ga_ids)
+ if 'javascript' in interview.external_files:
+ for packageref, fileref in interview.external_files['javascript']:
+ the_url = get_url_from_file_reference(fileref, {"_package": packageref})
+ if the_url is not None:
+ if is_js:
+ append_script_urls.append(get_url_from_file_reference(fileref, {"_package": packageref}))
+ else:
+ scripts += "\n" + f' '
+ else:
+ logmessage("index: could not find javascript file " + str(fileref))
+ if interview_status.question.checkin is not None:
+ do_action = interview_status.question.checkin
+ else:
+ do_action = None
+ chat_available = user_dict['_internal']['livehelp']['availability']
+ chat_mode = user_dict['_internal']['livehelp']['mode']
+ if chat_available == 'unavailable':
+ chat_status = 'off'
+ update_session(yaml_filename, chatstatus='off')
+ elif chat_available == 'observeonly':
+ chat_status = 'observeonly'
+ update_session(yaml_filename, chatstatus='observeonly')
+ else:
+ chat_status = session_info['chatstatus']
+ if chat_status in ('ready', 'on'):
+ chat_status = 'ringing'
+ update_session(yaml_filename, chatstatus='ringing')
+ if chat_status != 'off':
+ send_changes = True
+ else:
+ send_changes = bool(do_action is not None)
+ if current_user.is_authenticated:
+ user_id_string = str(current_user.id)
+ is_user = bool(not current_user.has_role('admin', 'developer', 'advocate'))
+ else:
+ user_id_string = 't' + str(session['tempuser'])
+ is_user = True
+ being_controlled = bool(r.get('da:control:uid:' + str(user_code) + ':i:' + str(yaml_filename) + ':userid:' + str(the_user_id)) is not None)
+ if debug_mode:
+ debug_readability_help = True
+ debug_readability_question = True
+ else:
+ debug_readability_help = False
+ debug_readability_question = False
+ force_full_screen = bool(interview.force_fullscreen is True or (re.search(r'mobile', str(interview.force_fullscreen).lower()) and is_mobile_or_tablet()))
+ the_checkin_interval = interview.options.get('checkin interval', CHECKIN_INTERVAL)
+ page_sep = "#page"
+ if refer is None:
+ location_bar = url_for('interview.index', **index_params)
+ elif refer[0] in ('start', 'run'):
+ location_bar = url_for('interview.run_interview_in_package', package=refer[1], filename=refer[2], **remove_i_from_dict(index_params))
+ page_sep = "#/"
+ elif refer[0] in ('start_dispatch', 'run_dispatch'):
+ location_bar = url_for('interview.run_interview', dispatch=refer[1], **remove_i_from_dict(index_params))
+ page_sep = "#/"
+ elif refer[0] in ('start_directory', 'run_directory'):
+ location_bar = url_for('interview.run_interview_in_package_directory', package=refer[1], directory=refer[2], filename=refer[3], **remove_i_from_dict(index_params))
+ page_sep = "#/"
+ else:
+ location_bar = None
+ for k, v in daconfig['dispatch'].items():
+ if v == yaml_filename:
+ location_bar = url_for('interview.run_interview', dispatch=k, **remove_i_from_dict(index_params))
+ page_sep = "#/"
+ break
+ if location_bar is None:
+ location_bar = url_for('interview.index', **index_params)
+ index_params_external = copy.copy(index_params)
+ index_params_external['_external'] = True
+ if interview_status.question.language != '*':
+ interview_language = interview_status.question.language
+ else:
+ interview_language = current_language
+ validation_rules = {'rules': {}, 'messages': {}, 'errorClass': 'da-has-error invalid-feedback', 'debug': False}
+ interview_status.exit_url = title_info.get('exit url', None)
+ interview_status.exit_link = title_info.get('exit link', 'leave')
+ interview_status.exit_label = title_info.get('exit label', word('Exit'))
+ interview_status.title = title_info.get('full', default_title)
+ interview_status.display_title = title_info.get('logo', interview_status.title)
+ interview_status.tabtitle = title_info.get('tab', interview_status.title)
+ interview_status.short_title = title_info.get('short', title_info.get('full', default_short_title))
+ interview_status.display_short_title = title_info.get('short logo', title_info.get('logo', interview_status.short_title))
+ interview_status.title_url = title_info.get('title url', None)
+ interview_status.title_url_opens_in_other_window = title_info.get('title url opens in other window', True)
+ interview_status.nav_item = title_info.get('navigation bar html', '')
+ the_main_page_parts = main_page_parts.get(interview_language, main_page_parts.get('*'))
+ interview_status.pre = title_info.get('pre', the_main_page_parts['main page pre'])
+ interview_status.post = title_info.get('post', the_main_page_parts['main page post'])
+ interview_status.footer = title_info.get('footer', the_main_page_parts['main page footer'] or get_part('global footer'))
+ if interview_status.footer:
+ interview_status.footer = re.sub(r'?p.*?>', '', str(interview_status.footer), flags=re.IGNORECASE).strip()
+ if interview_status.footer == 'off':
+ interview_status.footer = ''
+ interview_status.submit = title_info.get('submit', the_main_page_parts['main page submit'])
+ interview_status.back = title_info.get('back button label', the_main_page_parts['main page back button label'] or interview_status.question.back())
+ interview_status.cornerback = title_info.get('corner back button label', the_main_page_parts['main page corner back button label'] or interview_status.question.back())
+ bootstrap_theme = interview.get_bootstrap_theme()
+ if interview_status.question.question_type == "signature":
+ if interview.options.get('hide navbar', False):
+ bodyclass = "dasignature navbarhidden"
+ else:
+ bodyclass = "dasignature da-pad-for-navbar"
+ else:
+ if interview.options.get('hide navbar', False):
+ bodyclass = "dabody"
+ else:
+ bodyclass = "dabody da-pad-for-navbar"
+ if 'cssClass' in interview_status.extras:
+ bodyclass += ' ' + re.sub(r'[^A-Za-z0-9\_]+', '-', interview_status.extras['cssClass'])
+ elif hasattr(interview_status.question, 'id'):
+ bodyclass += ' question-' + re.sub(r'[^A-Za-z0-9]+', '-', interview_status.question.id.lower())
+ if interview_status.footer:
+ bodyclass += ' da-pad-for-footer'
+ if not is_ajax:
+ social = copy.deepcopy(daconfig['social'])
+ if 'social' in interview.consolidated_metadata and isinstance(interview.consolidated_metadata['social'], dict):
+ populate_social(social, interview.consolidated_metadata['social'])
+ standard_header_start = standard_html_start(interview_language=interview_language, debug=debug_mode, bootstrap_theme=bootstrap_theme, page_title=interview_status.title, social=social, yaml_filename=yaml_filename)
+ if debug_mode:
+ interview_status.screen_reader_text = {}
+ if TTS_ENABLED and 'speak_text' in interview_status.extras and interview_status.extras['speak_text']:
+ interview_status.initialize_screen_reader()
+ util_language = get_language()
+ util_dialect = get_dialect()
+ util_voice = get_voice()
+ question_language = interview_status.question.language
+ if len(interview.translations) > 0:
+ the_language = util_language
+ elif question_language != '*':
+ the_language = question_language
+ else:
+ the_language = util_language
+ if voicerss_config and 'language map' in voicerss_config and isinstance(voicerss_config['language map'], dict) and the_language in voicerss_config['language map']:
+ the_language = voicerss_config['language map'][the_language]
+ if the_language == util_language and util_dialect is not None:
+ the_dialect = util_dialect
+ elif voicerss_config and 'dialects' in voicerss_config and isinstance(voicerss_config['dialects'], dict) and the_language in voicerss_config['dialects']:
+ the_dialect = voicerss_config['dialects'][the_language]
+ elif the_language in valid_voicerss_dialects:
+ the_dialect = valid_voicerss_dialects[the_language][0]
+ else:
+ logmessage("index: unable to determine dialect; reverting to default")
+ the_language = DEFAULT_LANGUAGE
+ the_dialect = DEFAULT_DIALECT
+ if the_language == util_language and the_dialect == util_dialect and util_voice is not None:
+ the_voice = util_voice
+ elif voicerss_config and 'voices' in voicerss_config and isinstance(voicerss_config['voices'], dict) and the_language in voicerss_config['voices'] and isinstance(voicerss_config['voices'][the_language], dict) and the_dialect in voicerss_config['voices'][the_language]:
+ the_voice = voicerss_config['voices'][the_language][the_dialect]
+ elif voicerss_config and 'voices' in voicerss_config and isinstance(voicerss_config['voices'], dict) and the_language in voicerss_config['voices'] and isinstance(voicerss_config['voices'][the_language], str):
+ the_voice = voicerss_config['voices'][the_language]
+ elif the_language == DEFAULT_LANGUAGE and the_dialect == DEFAULT_DIALECT:
+ the_voice = DEFAULT_VOICE
+ else:
+ the_voice = None
+ for question_type in ('question', 'help'):
+ for audio_format in ('mp3', 'ogg'):
+ interview_status.screen_reader_links[question_type].append([url_for('tts.speak_file', i=yaml_filename, question=interview_status.question.number, digest='XXXTHEXXX' + question_type + 'XXXHASHXXX', type=question_type, format=audio_format, language=the_language, dialect=the_dialect, voice=the_voice or ''), audio_mimetype_table[audio_format]])
+ if (not validated) and the_question.name == interview_status.question.name:
+ for def_key, def_val in new_values.items():
+ safe_def_key = safeid(def_key)
+ if isinstance(def_val, list):
+ def_val = '[' + ','.join(def_val) + ']'
+ if safe_def_key in all_field_numbers:
+ for number in all_field_numbers[safe_def_key]:
+ try:
+ interview_status.defaults[number] = eval(def_val, pre_user_dict)
+ except:
+ pass
+ else:
+ try:
+ interview_status.other_defaults[def_key] = eval(def_val, pre_user_dict)
+ except:
+ pass
+ the_field_errors = field_error
+ else:
+ the_field_errors = None
+ # restore this, maybe
+ # if next_action_to_set:
+ # interview_status.next_action.append(next_action_to_set)
+ if next_action_to_set:
+ if 'event_stack' not in user_dict['_internal']:
+ user_dict['_internal']['event_stack'] = {}
+ session_uid = interview_status.current_info['user']['session_uid']
+ if session_uid not in user_dict['_internal']['event_stack']:
+ user_dict['_internal']['event_stack'][session_uid] = []
+ already_there = False
+ for event_item in user_dict['_internal']['event_stack'][session_uid]:
+ if event_item['action'] == next_action_to_set['action']:
+ already_there = True
+ break
+ if not already_there:
+ user_dict['_internal']['event_stack'][session_uid].insert(0, next_action_to_set)
+ if interview.use_progress_bar and (interview_status.question.progress is None or interview_status.question.progress >= 0):
+ the_progress_bar = progress_bar(user_dict['_internal']['progress'], interview)
+ else:
+ the_progress_bar = None
+ if interview.use_navigation and user_dict['nav'].visible():
+ if interview.use_navigation_on_small_screens == 'dropdown':
+ current_dict = {}
+ dropdown_nav_bar = navigation_bar(user_dict['nav'], interview, wrapper=False, a_class='dropdown-item', hide_inactive_subs=False, always_open=True, return_dict=current_dict)
+ if dropdown_nav_bar != '':
+ dropdown_nav_bar = ' \n \n \n \n \n \n'
+ else:
+ dropdown_nav_bar = ''
+ if interview.use_navigation == 'horizontal':
+ if interview.use_navigation_on_small_screens is not True:
+ nav_class = ' d-none d-md-block'
+ else:
+ nav_class = ''
+ the_nav_bar = navigation_bar(user_dict['nav'], interview, wrapper=False, inner_div_class='nav flex-row justify-content-center align-items-center nav-pills danav danavlinks danav-horiz danavnested-horiz')
+ if the_nav_bar != '':
+ the_nav_bar = dropdown_nav_bar + ' \n \n \n
\n \n'
+ else:
+ if interview.use_navigation_on_small_screens == 'dropdown':
+ if dropdown_nav_bar:
+ horiz_nav_bar = dropdown_nav_bar + '\n \n \n'
+ else:
+ horiz_nav_bar = ''
+ elif interview.use_navigation_on_small_screens:
+ horiz_nav_bar = navigation_bar(user_dict['nav'], interview, wrapper=False, inner_div_class='nav flex-row justify-content-center align-items-center nav-pills danav danavlinks danav-horiz danavnested-horiz')
+ if horiz_nav_bar != '':
+ horiz_nav_bar = dropdown_nav_bar + ' \n \n \n \n \n'
+ else:
+ horiz_nav_bar = ''
+ the_nav_bar = navigation_bar(user_dict['nav'], interview)
+ if the_nav_bar != '':
+ if interview.use_navigation == 'horizontal':
+ interview_status.using_navigation = 'horizontal'
+ else:
+ interview_status.using_navigation = 'vertical'
+ else:
+ interview_status.using_navigation = False
+ else:
+ the_nav_bar = ''
+ interview_status.using_navigation = False
+ content = as_html(interview_status, debug_mode, url_for('interview.index', **index_params), validation_rules, the_field_errors, the_progress_bar, steps - user_dict['_internal']['steps_offset'])
+ if debug_mode:
+ readability = {}
+ for question_type in ('question', 'help'):
+ if question_type not in interview_status.screen_reader_text:
+ continue
+ phrase = to_text(interview_status.screen_reader_text[question_type])
+ if (not phrase) or len(phrase) < 10:
+ phrase = "The sky is blue."
+ phrase = re.sub(r'[^A-Za-z 0-9\.\,\?\#\!\%\&\(\)]', r' ', phrase)
+ readability[question_type] = [('Flesch Reading Ease', textstat.flesch_reading_ease(phrase)),
+ ('Flesch-Kincaid Grade Level', textstat.flesch_kincaid_grade(phrase)),
+ ('Gunning FOG Scale', textstat.gunning_fog(phrase)),
+ ('SMOG Index', textstat.smog_index(phrase)),
+ ('Automated Readability Index', textstat.automated_readability_index(phrase)),
+ ('Coleman-Liau Index', textstat.coleman_liau_index(phrase)),
+ ('Linsear Write Formula', textstat.linsear_write_formula(phrase)),
+ ('Dale-Chall Readability Score', textstat.dale_chall_readability_score(phrase)),
+ ('Readability Consensus', textstat.text_standard(phrase))]
+ readability_report = ''
+ for question_type in ('question', 'help'):
+ if question_type in readability:
+ readability_report += ' ' + "\n"
+ if TTS_ENABLED and interview_status.using_screen_reader:
+ for question_type in ('question', 'help'):
+ if question_type not in interview_status.screen_reader_text:
+ continue
+ phrase = to_text(interview_status.screen_reader_text[question_type])
+ if encrypted:
+ the_phrase = encrypt_phrase(phrase, secret)
+ else:
+ the_phrase = pack_phrase(phrase)
+ the_hash = MD5Hash(data=phrase).hexdigest()
+ content = re.sub(r'XXXTHEXXX' + question_type + 'XXXHASHXXX', the_hash, content)
+ manage_tts_objects(0, interview_status=interview_status, yaml_filename=yaml_filename, user_code=user_code, phrase=phrase, the_phrase=the_phrase, the_hash=the_hash, question_type=question_type, the_language=the_language, the_dialect=the_dialect, encrypted=encrypted, the_voice=the_voice, secret=secret)
+ append_css_urls = []
+ if not is_ajax:
+ start_output = standard_header_start
+ if 'css' in interview.external_files:
+ for packageref, fileref in interview.external_files['css']:
+ the_url = get_url_from_file_reference(fileref, {"_package": packageref})
+ if the_url is not None:
+ if is_js:
+ append_css_urls.append(the_url)
+ else:
+ start_output += "\n" + ' '
+ else:
+ logmessage("index: could not find css file " + str(fileref))
+ if is_js:
+ scripts += additional_css(interview_status, js_only=True)
+ else:
+ start_output += current_app.config['GLOBAL_CSS'] + additional_css(interview_status)
+ start_output += '\n ' + interview_status.tabtitle + ' \n \n \n \n'
+ if interview.options.get('hide navbar', False):
+ output = make_navbar(interview_status, (steps - user_dict['_internal']['steps_offset']), interview.consolidated_metadata.get('show login', SHOW_LOGIN), user_dict['_internal']['livehelp'], debug_mode, index_params, extra_class='dainvisible')
+ else:
+ output = make_navbar(interview_status, (steps - user_dict['_internal']['steps_offset']), interview.consolidated_metadata.get('show login', SHOW_LOGIN), user_dict['_internal']['livehelp'], debug_mode, index_params)
+ output += flash_content + ' ' + "\n " + '' + "\n"
+ if the_nav_bar != '':
+ if interview_status.using_navigation == 'vertical':
+ output += horiz_nav_bar
+ output += the_nav_bar
+ output += content
+ if 'rightText' in interview_status.extras:
+ if interview_status.using_navigation == 'vertical':
+ output += ' \n"
+ if interview_status.question.question_type != "signature" and interview_status.post:
+ output += ' ' + "\n"
+ if interview_status.using_navigation == 'vertical':
+ output += '