From 016393357498d46c44f5877085f88e3f9162ed06 Mon Sep 17 00:00:00 2001 From: Jonathan Pyle Date: Tue, 23 Jan 2024 23:35:25 -0500 Subject: [PATCH] added raw html field type; added module to docassemble.demo demonstrating how raw html can be used to insert Bootstrap accordion HTML --- CHANGELOG.md | 7 ++ .../base/data/questions/examples/raw-html.yml | 16 ++++ docassemble_base/docassemble/base/parse.py | 22 +++-- .../docassemble/base/standardformatter.py | 30 ++++-- .../docassemble/demo/accordion.py | 92 +++++++++++++++++++ .../questions/examples/testaccordion1.yml | 89 ++++++++++++++++++ .../questions/examples/testaccordion2.yml | 39 ++++++++ .../questions/examples/testaccordion3.yml | 63 +++++++++++++ 8 files changed, 342 insertions(+), 16 deletions(-) create mode 100644 docassemble_base/docassemble/base/data/questions/examples/raw-html.yml create mode 100644 docassemble_demo/docassemble/demo/accordion.py create mode 100644 docassemble_demo/docassemble/demo/data/questions/examples/testaccordion1.yml create mode 100644 docassemble_demo/docassemble/demo/data/questions/examples/testaccordion2.yml create mode 100644 docassemble_demo/docassemble/demo/data/questions/examples/testaccordion3.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 712576e10..cecb25383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [1.4.94] - 2024-01-23 + +### Added +- The `raw html` special field type under `fields` and `review`. This + is similar to `html` but allows modification of the structure of the + HTML in the list as a whole. + ## [1.4.93] - 2024-01-20 ### Added diff --git a/docassemble_base/docassemble/base/data/questions/examples/raw-html.yml b/docassemble_base/docassemble/base/data/questions/examples/raw-html.yml new file mode 100644 index 000000000..6f300b1ce --- /dev/null +++ b/docassemble_base/docassemble/base/data/questions/examples/raw-html.yml @@ -0,0 +1,16 @@ +metadata: + title: Raw HTML + documentation: "https://docassemble.org/docs/fields.html#raw html" +--- +question: | + What is your favorite fruit? +fields: + - raw html: | +
+ - Favorite fruit: favorite_fruit + - raw html: | +
+--- +mandatory: True +question: | + Your favorite fruit is ${ favorite_fruit }. diff --git a/docassemble_base/docassemble/base/parse.py b/docassemble_base/docassemble/base/parse.py index 62f7167c9..bf032e6cf 100644 --- a/docassemble_base/docassemble/base/parse.py +++ b/docassemble_base/docassemble/base/parse.py @@ -1312,6 +1312,8 @@ def as_data(self, the_user_dict, encode=True): the_field['note'] = docassemble.base.filter.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']: + the_field['raw html'] = self.extras['raw html'][field.number] if field.number in self.hints: the_field['hint'] = self.hints[field.number] if debug: @@ -3950,8 +3952,8 @@ def __init__(self, orig_data, caller, **kwargs): continue if 'object labeler' in field and ('datatype' not in field or not field['datatype'].startswith('object')): raise DAError("An object labeler can only be used with an object data type") - if 'note' in field and 'html' in field: - raise DAError("You cannot include both note and html in a field." + self.idebug(data)) + if ('note' in field and 'html' in field) or ('note' in field and 'raw html' in field) or ('html' in field and 'raw html' in field): + raise DAError("You cannot combine note, html, and/or raw html in a single field." + self.idebug(data)) for key in field: if key == 'default' and 'datatype' in field and field['datatype'] in ('object', 'object_radio', 'object_multiselect', 'object_checkboxes'): continue @@ -4401,7 +4403,7 @@ def __init__(self, orig_data, caller, **kwargs): for x in field['exclude']: self.find_fields_in(x) field_info['selections']['exclude'].append(compile(x, '', 'eval')) - elif key in ('note', 'html'): + elif key in ('note', 'html', 'raw html'): if 'extras' not in field_info: field_info['extras'] = {} field_info['extras'][key] = TextObject(definitions + str(field[key]), question=self) @@ -4553,9 +4555,11 @@ def __init__(self, orig_data, caller, **kwargs): self.fields_used.add(field_info['saveas']) else: self.other_fields_used.add(field_info['saveas']) - elif 'note' in field or 'html' in field: + elif 'note' in field or 'html' or 'raw html' in field: if 'note' in field: field_info['type'] = 'note' + elif 'raw html' in field: + field_info['type'] = 'raw html' else: field_info['type'] = 'html' self.fields.append(Field(field_info)) @@ -4595,7 +4599,7 @@ def __init__(self, orig_data, caller, **kwargs): if not isinstance(field[key], dict) and not isinstance(field[key], list): field_info['help'] = TextObject(definitions + str(field[key]), question=self) field_info['type'] = 'button' - elif key in ('note', 'html'): + elif key in ('note', 'html', 'raw html'): if 'type' not in field_info: field_info['type'] = key if 'extras' not in field_info: @@ -4769,7 +4773,7 @@ def __init__(self, orig_data, caller, **kwargs): self.find_fields_in(the_saveas) if 'action' in field: field_info['action'] = {'action': field['action'], 'arguments': {}} - if 'type' in field_info and field_info['type'] in ('note', 'html') and 'label' in field_info: + if 'type' in field_info and field_info['type'] in ('note', 'html', 'raw html') and 'label' in field_info: del field_info['type'] if len(field_info['data']) > 0: if 'saveas_code' not in field_info: @@ -4780,7 +4784,7 @@ def __init__(self, orig_data, caller, **kwargs): field_info['action'] = {'action': field_info['data'][0], 'arguments': {}} else: field_info['action'] = {'action': "_da_force_ask", 'arguments': {'variables': field_info['data']}} - if len(field_info['data']) > 0 or ('type' in field_info and field_info['type'] in ('note', 'html')): + if len(field_info['data']) > 0 or ('type' in field_info and field_info['type'] in ('note', 'html', 'raw html')): self.fields.append(Field(field_info)) else: raise DAError("A field in a review list was listed without indicating a label or a variable name, and the field was not a note or raw HTML." + self.idebug(field_info)) @@ -5988,7 +5992,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p continue else: extras['field metadata'][field.number] = recursive_eval_textobject_or_primitive(field.extras['field metadata'], user_dict) - for key in ('note', 'html', 'min', 'max', 'minlength', 'maxlength', 'step', 'scale', 'inline', 'inline width', 'currency symbol', 'pen color', 'file css class'): # 'script', 'css', + for key in ('note', 'html', 'raw html', 'min', 'max', 'minlength', 'maxlength', 'step', 'scale', 'inline', 'inline width', 'currency symbol', 'pen color', 'file css class'): # 'script', 'css', if key in field.extras: if key not in extras: extras[key] = {} @@ -6577,7 +6581,7 @@ def ask(self, user_dict, old_user_dict, the_x, iterators, sought, orig_sought, p if 'field metadata' not in extras: extras['field metadata'] = {} extras['field metadata'][field.number] = recursive_eval_textobject_or_primitive(field.extras['field metadata'], user_dict) - for key in ('note', 'html', 'min', 'max', 'minlength', 'maxlength', 'show_if_val', 'step', 'scale', 'inline', 'inline width', 'ml_group', 'currency symbol', 'css class', 'pen color', 'file css class'): # , 'textresponse', 'content_type' # 'script', 'css', + for key in ('note', 'html', 'raw html', 'min', 'max', 'minlength', 'maxlength', 'show_if_val', 'step', 'scale', 'inline', 'inline width', 'ml_group', 'currency symbol', 'css class', 'pen color', 'file css class'): # , 'textresponse', 'content_type' # 'script', 'css', if key in field.extras: if key not in extras: extras[key] = {} diff --git a/docassemble_base/docassemble/base/standardformatter.py b/docassemble_base/docassemble/base/standardformatter.py index 25e7cef32..492e7f113 100644 --- a/docassemble_base/docassemble/base/standardformatter.py +++ b/docassemble_base/docassemble/base/standardformatter.py @@ -279,7 +279,7 @@ def as_sms(status, the_user_dict, links=None, menu_items=None): if hasattr(the_field, 'datatype'): if the_field.datatype in ['script', 'css']: # why did I ever comment this out? continue - if the_field.datatype in ['html', 'note'] and field is not None: + if the_field.datatype in ['html', 'raw html', 'note'] and field is not None: continue if the_field.datatype == 'note': info_message = to_text(markdown_to_html(status.extras['note'][the_field.number], status=status), terms, links) @@ -287,6 +287,9 @@ def as_sms(status, the_user_dict, links=None, menu_items=None): if the_field.datatype == 'html': info_message = to_text(process_target(status.extras['html'][the_field.number].rstrip()), terms, links) continue + if the_field.datatype == 'raw html': + info_message = to_text(process_target(status.extras['raw html'][the_field.number].rstrip()), terms, links) + continue # logmessage("field number is " + str(the_field.number)) if not hasattr(the_field, 'saveas'): logmessage("as_sms: field has no saveas") @@ -328,7 +331,7 @@ def as_sms(status, the_user_dict, links=None, menu_items=None): elif hasattr(immediate_next_field, 'datatype'): if immediate_next_field.datatype in ['note']: next_label = ' (' + word("Next will be") + ' ' + to_text(markdown_to_html(status.extras['note'][immediate_next_field.number], trim=False, status=status, strip_newlines=True), terms, links) + ')' - elif immediate_next_field.datatype in ['html']: + elif immediate_next_field.datatype in ['html', 'raw html']: next_label = ' (' + word("Next will be") + ' ' + to_text(status.extras['html'][immediate_next_field.number].rstrip(), terms, links) + ')' if hasattr(field, 'label') and status.labels[field.number] != "no label": label = to_text(markdown_to_html(status.labels[field.number], trim=False, status=status, strip_newlines=True), terms, links) @@ -1011,6 +1014,8 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar for field in status.get_field_list(): if 'html' in status.extras and field.number in status.extras['html']: side_note_content = status.extras['html'][field.number].rstrip() + elif 'raw html' in status.extras and field.number in status.extras['raw html']: + side_note_content = status.extras['raw html'][field.number].rstrip() elif 'note' in status.extras and field.number in status.extras['note']: side_note_content = markdown_to_html(status.extras['note'][field.number], status=status, strip_newlines=True) else: @@ -1044,6 +1049,9 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar else: fieldlist.append('
' + side_note_content + '
\n') continue + if field.datatype == 'raw html' and 'raw html' in status.extras and field.number in status.extras['raw html'] and side_note_content: + fieldlist.append(' ' + side_note_content + '\n') + continue if field.datatype == 'note' and 'note' in status.extras and field.number in status.extras['note']: if field.number in status.helptexts: if tabular: @@ -1175,6 +1183,8 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar for field in field_list: if 'html' in status.extras and field.number in status.extras['html']: note_fields[field.number] = process_target(status.extras['html'][field.number].rstrip()) + elif 'raw html' in status.extras and field.number in status.extras['raw html']: + note_fields[field.number] = process_target(status.extras['raw html'][field.number].rstrip()) elif 'note' in status.extras and field.number in status.extras['note']: note_fields[field.number] = markdown_to_html(status.extras['note'][field.number], status=status, embedder=embed_input) if hasattr(field, 'saveas'): @@ -1339,13 +1349,19 @@ def as_html(status, debug, root, validation_rules, field_error, the_progress_bar fieldlist.append('

' + list_message + ' ' + word("(Deleted)") + '' + da_remove_existing + '
\n') else: if field.number in note_fields: - if field.number in status.helptexts: - fieldlist.append(field_item(field, grid_info, pre=style_def + data_def, classes='da-field-container da-field-container-note' + class_def + extra_container_class, content_classes='col', content=help_wrap(note_fields[field.number], status.helptexts[field.number], status), under_text=under_text)) - # fieldlist.append('
' + help_wrap(note_fields[field.number], status.helptexts[field.number], status) + '
\n') + if field.datatype == 'raw html': + fieldlist.append(note_fields[field.number]) else: - fieldlist.append(field_item(field, grid_info, pre=style_def + data_def, classes='da-field-container da-field-container-note' + class_def + extra_container_class, content_classes='col', content='
' + note_fields[field.number] + '
', under_text=under_text)) - # fieldlist.append('
' + note_fields[field.number] + '
\n') + if field.number in status.helptexts: + fieldlist.append(field_item(field, grid_info, pre=style_def + data_def, classes='da-field-container da-field-container-note' + class_def + extra_container_class, content_classes='col', content=help_wrap(note_fields[field.number], status.helptexts[field.number], status), under_text=under_text)) + # fieldlist.append('
' + help_wrap(note_fields[field.number], status.helptexts[field.number], status) + '
\n') + else: + fieldlist.append(field_item(field, grid_info, pre=style_def + data_def, classes='da-field-container da-field-container-note' + class_def + extra_container_class, content_classes='col', content='
' + note_fields[field.number] + '
', under_text=under_text)) + # fieldlist.append('
' + note_fields[field.number] + '
\n') # continue + elif field.datatype == 'raw html': + if field.number in note_fields: + fieldlist.append(note_fields[field.number]) elif field.datatype == 'note': if field.number in note_fields: if field.number in status.helptexts: diff --git a/docassemble_demo/docassemble/demo/accordion.py b/docassemble_demo/docassemble/demo/accordion.py new file mode 100644 index 000000000..554f0f1a7 --- /dev/null +++ b/docassemble_demo/docassemble/demo/accordion.py @@ -0,0 +1,92 @@ +# do not pre-load +import random +import re +import string + +__all__ = ['start_accordion', 'next_accordion', 'end_accordion'] + +r = random.SystemRandom() + + +def _get_section_id(section): + section_id = re.sub(r'[^a-z0-9]+', '-', section.lower()) + if len(section_id) > 0: + section_id += '-' + section_id += (''.join(r.choice(string.ascii_lowercase) for i in range(5))) + return section_id + + +def _header(section_id): + return f"""\ +
+
+
+ """ + + +def _section_start(section_id, section, showing): + if showing: + show = ' show' + expanded = 'true' + collapsed = '' + else: + show = '' + expanded = 'false' + collapsed = ' collapsed' + return f"""\ +
+

+ +

+
+
+""" + + +def _section_end(): + return """\ +
+
+
+""" + + +def _footer(): + return """\ +
+
+
+ """ + + +def start_accordion(section, showing=False): + """Returns HTML for the start of a series of accordion + sections. The end_accordion() function must be called at some + point later in the page, or else the HTML will be corrupted. If + you want the section to be open when the page loads, set + showing=True. + + """ + section_id = _get_section_id(section) + return _header(section_id) + _section_start(section_id, section, showing) + + +def next_accordion(section, showing=False): + """Returns HTML for ending a previous according section and + starting a new accordion section. Can only be used after + start_accordion(). If you want the section to be open when the + page loads, set showing=True. + + """ + section_id = _get_section_id(section) + return _section_end() + _section_start(section_id, section, showing) + + +def end_accordion(): + """Returns HTML for ending a series of accordion sections. Can + only be used after next_accordion or start_accordion(). + + """ + return _section_end() + _footer() diff --git a/docassemble_demo/docassemble/demo/data/questions/examples/testaccordion1.yml b/docassemble_demo/docassemble/demo/data/questions/examples/testaccordion1.yml new file mode 100644 index 000000000..59cc329da --- /dev/null +++ b/docassemble_demo/docassemble/demo/data/questions/examples/testaccordion1.yml @@ -0,0 +1,89 @@ +metadata: + title: Accordion Review + short title: Accordion + documentation: "https://docassemble.org/docs/recipes.html#accordion" + example start: 2 + example end: 3 +--- +mandatory: True +code: | + favorite_cat = 'Tabby' + favorite_dog = 'Spaniel' + favorite_fruit = 'Apple' + favorite_vegetable = 'Turnip' + fashion_aesthetic = 'Goth' + decor_aesthetic = 'Victorian' +--- +modules: + - docassemble.demo.accordion +--- +event: review_screen +question: | + Please review your answers. +review: + - raw html: | + ${ start_accordion('Pets', showing=True) } + - Edit: favorite_cat + button: | + You said your favorite cat was + **${ favorite_fruit }**. + - Edit: favorite_vegetable + button: | + You said your favorite dog was + **${ favorite_dog }**. + - raw html: | + ${ next_accordion('Food') } + - Edit: favorite_fruit + button: | + You said your favorite fruit was + **${ favorite_fruit }**. + - Edit: favorite_vegetable + button: | + You said your favorite vegetable was + **${ favorite_vegetable }**. + - raw html: | + ${ next_accordion('Aesthetics') } + - Edit: fashion_aesthetic + button: | + You said your fashion aesthetic was + **${ fashion_aesthetic }**. + - Edit: decor_aesthetic + button: | + You said your home decor aesthetic was + **${ decor_aesthetic }**. + - raw html: | + ${ end_accordion() } +--- +mandatory: True +code: | + review_screen +--- +question: | + What is your favorite cat? +fields: + - Favorite cat: favorite_cat +--- +question: | + What is your favorite dog? +fields: + - Favorite dog: favorite_dog +--- +question: | + What is your favorite fruit? +fields: + - Favorite fruit: favorite_fruit +--- +question: | + What is your favorite vegetable? +fields: + - Favorite vegetable: favorite_vegetable +--- +question: | + How would you describe your preferred fashion for clothing? +fields: + - Fashion aesthetic: fashion_aesthetic +--- +question: | + How would you describe your choice of home decor? +fields: + - Decor aesthetic: decor_aesthetic diff --git a/docassemble_demo/docassemble/demo/data/questions/examples/testaccordion2.yml b/docassemble_demo/docassemble/demo/data/questions/examples/testaccordion2.yml new file mode 100644 index 000000000..5f69ad31e --- /dev/null +++ b/docassemble_demo/docassemble/demo/data/questions/examples/testaccordion2.yml @@ -0,0 +1,39 @@ +metadata: + title: Accordion Fields + short title: Accordion + documentation: "https://docassemble.org/docs/recipes.html#accordion" + example start: 1 + example end: 2 +--- +modules: + - docassemble.demo.accordion +--- +question: | + Tell me about your preferences +fields: + - raw html: | + ${ start_accordion('Pets', showing=True) } + - Favorite cat: favorite_cat + - Favorite dog: favorite_dog + - raw html: | + ${ next_accordion('Food') } + - Favorite fruit: favorite_fruit + required: False + - Favorite vegetable: favorite_vegetable + required: False + - Favorite meat dish: favorite_meat_dish + show if: + variable: favorite_dog + is: spaniel + - raw html: | + ${ next_accordion('Aesthetics') } + - Fashion aesthetic: fashion_aesthetic + required: False + - Decor aesthetic: decor_aesthetic + required: False + - raw html: ${ end_accordion() } +--- +mandatory: True +question: | + Your favorite cat is ${ favorite_cat } and + your favorite dog is ${ favorite_dog }. diff --git a/docassemble_demo/docassemble/demo/data/questions/examples/testaccordion3.yml b/docassemble_demo/docassemble/demo/data/questions/examples/testaccordion3.yml new file mode 100644 index 000000000..583806b04 --- /dev/null +++ b/docassemble_demo/docassemble/demo/data/questions/examples/testaccordion3.yml @@ -0,0 +1,63 @@ +metadata: + title: Accordion + short title: Accordion + documentation: "https://docassemble.org/docs/recipes.html#accordion" + example start: 1 + example end: 2 +--- +modules: + - docassemble.demo.accordion +--- +question: | + Welcome to the interview. +subquestion: + This interview will determine a recommended direction + for your life. + + ${ start_accordion('What do I need to know before starting?') } + + Modernipsum dolor sit amet illusionism cubo-futurism international + gothic historicism, neo-minimalism divisionism cobra intervention + art art nouveau. Installation art futurism les nabis academic hudson + river school young british artists, romanticism neo-expressionism + street art orphism, lyrical abstraction avant-garde remodernism + vorticism. Divisionism caravaggisti die brücke tachisme + impressionism, gothic art luminism illusionism op art neoclassicism, + street art situationist international neoism. Orphism russian + symbolism academic ego-futurism kinetic art neo-dada dada stuckism + international gründerzeit, post-impressionism impressionism + postmodernism maximalism precisionism post-painterly + abstraction. Russian symbolism superflat new media art jugendstil + maximalism illusionism, gründerzeit scuola romana merovingian + rayonism secularism, existentialism op art action painting lyrical + abstraction. + + ${ next_accordion('What do I do after I finish the interview?') } + + Metaphysical art barbizon school carolingian neo-minimalism + primitivism superflat neo-minimalism naturalism, der blaue reiter + hard-edge painting new media art fluxus superstroke + monumentalism. Art deco russian futurism cubo-futurism pop art + relational art neo-expressionism, synchromism pre-raphaelites sound + art photorealism classicism, surrealism gothic art hudson river + school scuola romana. Rococo biedermeier cloisonnism secularism + hudson river school fluxus, modernism, ego-futurism formalism + manierism, gründerzeit deformalism abstract expressionism + postmodernism. Classicism postminimalism superstroke lowbrow + tonalism fauvism color field painting systems art, biedermeier + post-impressionism hyperrealism structuralism neoclassicism. Dada + tachisme luminism manierism surrealism, avant-garde performance art + neoclassicism hard-edge painting neo-impressionism, nouveau realisme + eclecticism tonalism. + + ${ end_accordion() } +continue button field: intro +--- +mandatory: True +code: | + intro + final_screen +--- +event: final_screen +question: | + This concludes the interview.