├── .bumpversion.cfg ├── .env.example ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── hooks │ └── pre-commit └── workflows │ ├── black-action.yml │ ├── publish.yml │ ├── run_form_tests.yml │ └── unittests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── bumpversion.sh ├── docassemble ├── AssemblyLine │ ├── __init__.py │ ├── al_courts.py │ ├── al_document.py │ ├── al_general.py │ ├── custom_jinja_filters.py │ ├── data │ │ ├── questions │ │ │ ├── al_code.yml │ │ │ ├── al_document.yml │ │ │ ├── al_language.yml │ │ │ ├── al_package.yml │ │ │ ├── al_package_unstyled.yml │ │ │ ├── al_question_test.yml │ │ │ ├── al_reminders.yml │ │ │ ├── al_saved_sessions.yml │ │ │ ├── al_saved_sessions_store.yml │ │ │ ├── al_settings.yml │ │ │ ├── al_terms_of_use.yml │ │ │ ├── al_visual.yml │ │ │ ├── assembly_line.yml │ │ │ ├── assembly_line_unstyled.yml │ │ │ ├── demo_search_sessions.yml │ │ │ ├── feedback.yml │ │ │ ├── interview_list.yml │ │ │ ├── ql_baseline.yml │ │ │ ├── ql_catchall.yml │ │ │ ├── ql_namechange.yml │ │ │ ├── reserved_words.yml │ │ │ ├── test_al_table_document.yml │ │ │ ├── test_aladdendum.yml │ │ │ ├── test_alcourts.yml │ │ │ ├── test_aldocument.yml │ │ │ ├── test_aldocument_background_assembly.yml │ │ │ ├── test_alexhibit.yml │ │ │ ├── test_alexhibit_maxsize.yml │ │ │ ├── test_bundle_parity.yml │ │ │ ├── test_familiar_disambiguation.yml │ │ │ ├── test_question_library.yml │ │ │ └── test_signature.yml │ │ ├── sources │ │ │ ├── README.md │ │ │ ├── es-words.yml │ │ │ ├── languages.yml │ │ │ ├── test_aldocument.feature │ │ │ ├── test_alexhibit_docx_1.docx │ │ │ ├── test_alexhibit_jpg_1.jpg │ │ │ ├── test_alexhibit_pdf_1.pdf │ │ │ ├── test_alexhibit_png_1.png │ │ │ ├── test_question_library.feature │ │ │ ├── translation_es.xlsx │ │ │ ├── translation_ht.xlsx │ │ │ └── translation_pt.xlsx │ │ ├── static │ │ │ ├── README.md │ │ │ ├── al_audio.css │ │ │ ├── al_audio.js │ │ │ ├── aldocument.css │ │ │ ├── aldocument.js │ │ │ ├── interview_list.css │ │ │ ├── limit_upload_size.js │ │ │ ├── lt1-pullout-10-getting-organized.pdf │ │ │ ├── ma_logo.png │ │ │ ├── placeholder.png │ │ │ ├── seal.jpg │ │ │ └── styles.css │ │ └── templates │ │ │ ├── README.md │ │ │ ├── al_basic_addendum.docx │ │ │ ├── exhibit_cover_sheet.docx │ │ │ ├── exhibit_table_of_contents.docx │ │ │ ├── general_cover_sheet.docx │ │ │ ├── sample_word_template.docx │ │ │ ├── test_aldocument_docx_1.docx │ │ │ └── test_aldocument_pdf_1.pdf │ ├── language.py │ ├── project_maintenance.py │ ├── py.typed │ ├── requirements.txt │ ├── sessions.py │ ├── sign.py │ ├── test_al_document.py │ ├── test_al_general.py │ └── test_language.py └── __init__.py ├── pyproject.toml ├── setup.cfg └── setup.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.17.0 3 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P\d+))? 4 | serialize = 5 | {major}.{minor}.{patch}.{test} 6 | {major}.{minor}.{patch} 7 | commit = True 8 | tag = True 9 | 10 | [bumpversion:file:setup.py] 11 | search = '{current_version}' 12 | replace = '{new_version}' 13 | 14 | [bumpversion:file:docassemble/AssemblyLine/__init__.py] 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # See the `bumpversion.sh` for more information on these variables 2 | export TEAMS_BUMP_WEBHOOK="https://example.com/webhookb2/..." 3 | export TWINE_USERNAME="" 4 | export TWINE_PASSWORD="" 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Version Information (should be in `your.docassemble.server/updatepackage`:** 27 | - Docassemble Version: 28 | - AssemblyLine Version: 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | 8 | black . --check 9 | -------------------------------------------------------------------------------- /.github/workflows/black-action.yml: -------------------------------------------------------------------------------- 1 | name: formatting action 2 | on: 3 | # Trigger the workflow on push or pull request, 4 | # but only for the main branch 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | jobs: 12 | linter_name: 13 | name: formatting 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: SuffolkLITLab/ALActions/black-formatting@main 17 | with: 18 | MAKE_PR: "true" 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | - uses: SuffolkLITLab/ALActions/docsig@main -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI and announce to teams 2 | 3 | on: push 4 | 5 | jobs: 6 | build-n-publish: 7 | runs-on: ubuntu-24.04 8 | name: Build and publish Python 🐍 distributions 📦 to PyPI 9 | steps: 10 | - uses: SuffolkLITLab/ALActions/publish@main 11 | with: 12 | PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 13 | VERSION_TO_PUBLISH: ${{ env.GITHUB_REF_NAME }} 14 | TEAMS_BUMP_WEBHOOK: ${{ secrets.TEAMS_BUMP_WEBHOOK }} 15 | -------------------------------------------------------------------------------- /.github/workflows/run_form_tests.yml: -------------------------------------------------------------------------------- 1 | name: ALKiln v5 tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | tags-ignore: 8 | - "**" 9 | paths: 10 | - 'requirements.txt' 11 | - '**.py' 12 | - '**.yml' 13 | - 'docassemble/**' 14 | workflow_dispatch: 15 | inputs: 16 | tags: 17 | description: 'Optional. Use a "tag expression" specify which tagged tests to run. See https://cucumber.io/docs/cucumber/api/#tag-expressions for syntax.' 18 | default: '' 19 | show_docker_output: 20 | required: false 21 | default: false 22 | type: boolean 23 | description: 'Show the docker logs while building the GitHub server container. It will also save the docker log artifact. This might show sensitive config information.' 24 | # To run your tests on a schedule, delete the first "#" symbol at the beginning of each line below. 25 | ## Also see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule 26 | ## Also see https://crontab.guru/examples.html 27 | #schedule: 28 | # - cron: '0 1 * * TUE' 29 | 30 | jobs: 31 | 32 | alkiln-tests: 33 | runs-on: ubuntu-latest 34 | name: Run ALKiln tests 35 | steps: 36 | - uses: actions/checkout@v3 37 | - name: "ALKiln - Start the isolated temporary docassemble server on GitHub" 38 | id: github_server 39 | uses: suffolkLITLab/ALKiln/action_for_github_server@v5 40 | with: 41 | SHOW_DOCKER_OUTPUT: "${{ github.event.inputs.show_docker_output }}" 42 | - run: echo "ALKiln finished starting the isolated GitHub docassemble server" 43 | shell: bash 44 | - name: Use ALKiln to run tests 45 | uses: SuffolkLITLab/ALKiln@v5 46 | env: 47 | ALKILN_TAG_EXPRESSION: "${{ (github.event.inputs.tags && format('{0}', github.event.inputs.tags)) }}" 48 | with: 49 | SERVER_URL: "${{ steps.github_server.outputs.SERVER_URL }}" 50 | DOCASSEMBLE_DEVELOPER_API_KEY: "${{ steps.github_server.outputs.DOCASSEMBLE_DEVELOPER_API_KEY }}" 51 | INSTALL_METHOD: "server" 52 | # To learn more, see https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#optional-inputs 53 | ALKILN_TAG_EXPRESSION: "${{ env.ALKILN_TAG_EXPRESSION }}" 54 | - run: echo "Finished running ALKiln tests" 55 | 56 | ## To make a new issue in your repository when a test fails, 57 | ## simply delete the first "#" symbol in each line below 58 | #- name: If any tests failed create an issue 59 | # if: ${{ failure() }} 60 | # uses: actions-ecosystem/action-create-issue@v1 61 | # with: 62 | # github_token: "${{ secrets.github_token }}" 63 | # title: ALKiln tests failed 64 | # body: | 65 | # An ALKiln test failed. See the action at ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}. 66 | # labels: | 67 | # bug 68 | -------------------------------------------------------------------------------- /.github/workflows/unittests.yml: -------------------------------------------------------------------------------- 1 | name: Run python only unit tests 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | test-assemblyline: 9 | runs-on: ubuntu-latest 10 | name: Run python only unit tests 11 | steps: 12 | - uses: SuffolkLITLab/ALActions/pythontests@main -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/chromedriver 2 | *DS_Store* 3 | node_modules* 4 | .env 5 | .envrc 6 | npm-debug.log 7 | error*.jpg 8 | screenshot_*.jpg 9 | downloads_* 10 | *_lang_*.feature 11 | *_report_* 12 | _al_test_project_name.json 13 | .history 14 | 15 | # Python related 16 | __pycache__/ 17 | docassemble.AssemblyLine.egg-info/* 18 | **/*.pyi 19 | .mypy_cache/** 20 | **/.mypy_cache/** 21 | dist/** 22 | .venv/** 23 | 24 | # IDE related 25 | .vscode/** 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Since 2.14.0, the changelog details are in [GitHub's Releases](https://github.com/SuffolkLITLab/docassemble-AssemblyLine/releases); 4 | you can see the changes introduced in each release of the project there. 5 | 6 | Changelogs for versions before 2.14.0 are below. 7 | 8 | ## Version v2.13.0 9 | 10 | Added 11 | * Export to JSON button on "share" screen 12 | * Offer the user's address as default for second, third, etc user 13 | * New baseline questions for probate matters 14 | * new `show_if` parameter for all `*_fields()` methods on ALIndividuals 15 | * Added include reference to new ALToolbox InterviewStats module 16 | 17 | Changed 18 | * Better contrast for accessibility 19 | * "terms" now have a dotted underline instead of solid 20 | * Improvements to exhibit code (OCR, etc) 21 | * `send_button_html()` is no longer displayed if the bundle has no enabled documents 22 | * default value of `github_user` comes from configuration instead of defaulting to `suffolklitlab` 23 | * when the key is equal to `preview`, `_preview` is appended to filenames (to generate unique filenames in tests) 24 | * improvements to typing (mypy) 25 | 26 | Fixed 27 | * the default feedback form title had literal mako tags 28 | 29 | ## Version v2.11.3 30 | 31 | * Move the "answer set" feature behind a global configuration option (opt-in) 32 | * Accessibility improvements 33 | * Add PDF/A support to ALDocument class 34 | 35 | ## Version v2.11.2 36 | 37 | Incorrect reference to save session location in the new "Answer set" feature 38 | 39 | ## Version v2.11.1 40 | 41 | Correct the priority of the new default gender for a business; it was taking priority over the gender question 42 | 43 | ## Version v2.11.0 44 | 45 | New: 46 | 47 | * Added "answer set" feature that is available on production forms 48 | * Accessibility improvements 49 | * Minor API additions to improve developer experience with ALDocument class 50 | * `language_fields()` method 51 | * ALDocumentUpload class 52 | 53 | Fixed: 54 | 55 | * businesses have a default gender of "other" (helps language methods work correctly) 56 | * Added some missing objects for nouns created by the Weaver 57 | * Fixed label for "fax number" field 58 | * return proper value when `key` function called in an addendum file 59 | 60 | Changed: 61 | * Types are now checked for safe usage with mypy on commit 62 | 63 | ## Version v2.10.2 64 | 65 | Fixed: 66 | * More protection when using snapshot feature to avoid snapshotting files/file-like objects that can't be restored 67 | * fix some bad assumptions in the `as_editable_list()` method of ALDocumentBundle. Will work with all ALDocument subclasses now, including uploaded files 68 | 69 | ## Version v2.10.1 70 | 71 | New features: 72 | 73 | * added `full_names` method to ALPeopleList class (always uses full middle name, not middle initial in list) 74 | 75 | Bug fixes: 76 | 77 | * ensure definition of `AL_ORGANIZATION_TITLE` for feedback page 78 | * Fix issue where you are not prompted to add additional pages to the second or more exhibit with default questions 79 | 80 | ## Version v2.10.0 81 | 82 | Add questions for `previous_addresses` and `other_addresses` 83 | Fix preexisting bug with address methods--missing imports 84 | 85 | ## Version v2.9.0 86 | 87 | As of AssemblyLine v2.9.0, you can now include AssemblyLine code in your interview by 88 | referencing `docassemble.AssemblyLine:assembly_line.yml` instead of `docassemble.AssemblyLine:al_package.yml`. 89 | The old reference is deprecated but there are no current plans to remove it. 90 | 91 | * Bugfixes 92 | * Added additional comments and documentation of classes and variables 93 | * Slightly improved phrasing of some questions 94 | * .zip file includes DOCX files (configurable) 95 | * Better typing 96 | * Starting using the black autoreformatter to have more consistent Python coding style 97 | * Remove some debugging strings 98 | 99 | ## Version v2.8.0 100 | 101 | Bug fixes: 102 | * email template does not trigger any extra screens, making it safer to use outside of AssemblyLine interviews 103 | * Improvements to address question + mailing address defaults to home address 104 | * Moved the help template for user role into the subquestion part to improve readability 105 | * new version of ALKiln 106 | * Remove some instances of CourtFormsOnline.org and add new variables to allow this to be customized for different jurisdictions (AL_ORGANIZATION_TITLE and AL_ORGANIZATION_HOMEPAGE) 107 | * Small cleanups in ql_baseline.yml 108 | 109 | ## Version v2.7.1 110 | 111 | Use metadata title, not the action title in navigation bar 112 | 113 | ## Version v2.7.0 114 | 115 | * Add form title to the navigation bar per Theory & Principle UX audit 116 | * Limit upload size 117 | * Misc. accessibility fixes 118 | * Fix bug with signature screen 119 | * Make the "courtformsonline" a variable 120 | 121 | ## Version v2.6.2 122 | 123 | * Add "Start over" menu option 124 | * Merge and deprecate docassemble.LanguagePack - features are now part of AL Core 125 | 126 | ## Version v2.6.1 127 | 128 | Increased caching for some translated strings 129 | 130 | ## Version v2.6.0 131 | 132 | * Added new short_list method on ALPeopleList, to display abbreviated list of people 133 | * Remove some uses of question help button 134 | * send_button_html now triggers sending separate PDFs for each bundled item. Editable checkbox still sends Word DOCX files as appropriate 135 | * Improved bumpversion script link to changelog 136 | 137 | ## Version v2.5.0 138 | 139 | * Use separate PDFs in send_button_html 140 | * Remove some question help buttons, per usability feedback 141 | * Fix bumpversion script 142 | 143 | ## Version v2.4.1 144 | 145 | Add back in mistakenly removed AL_DEFAULT_OVERFLOW_MESSAGE 146 | 147 | ## Version v2.4.0 148 | 149 | Allow ALDocumentBundles to set the value of auto_gather and gathered in object declaration 150 | 151 | ## Version v2.3.8 152 | 153 | Correct usage of include_table_of_contents in ALExhibitDocument class 154 | 155 | ## Version v2.3.7 156 | 157 | Fixed bug in name_fields 158 | 159 | ## Version v2.3.6 160 | 161 | Make suffix an optional field to display in name_fields() method 162 | 163 | ## Version v2.3.5 164 | 165 | Hotfix error in country attribute of address_fields() method 166 | 167 | ## Version v2.3.4 168 | 169 | Fix #269 - make the ALExhibitDocument class idempotent 170 | 171 | ## Version v2.3.3 172 | 173 | Sort session names in Fast Forward (dev only feature) 174 | 175 | ## Version v2.3.1 176 | 177 | Safer string handling in send_button_html 178 | 179 | ## Version v2.3.0 180 | 181 | Add the ability for a developer to snapshot and fastforward sessions. This adds a HUD along with the question id 182 | 183 | ## Version v2.2.1 184 | 185 | Important data leakage fix (#263) 186 | 187 | ## Version v2.2.0 188 | 189 | * Added ALExhibitDocument class 190 | * Added automated tests 191 | * Improvements to handling of refreshed document state 192 | * Improvements to docstrings 193 | 194 | ## v2.1.4 195 | * Added ALStaticDocument class 196 | * Respect filename provided on download screens 197 | * Make submit buttons minimum of 8em wide 198 | * Added appeals_court placeholder question 199 | 200 | ## v2.1.2 201 | Added internationalization support to address fields methods, together with new constants `AL_DEFAULT_COUNTRY`, `AL_DEFAULT_STATE`, and `AL_DEFAULT_LANGUAGE` (use ISO 2 letter codes for all 3 parameters). 202 | 203 | ## v2.0.18 204 | 205 | The first tagged release of Assembly Line 206 | 207 | ## v2.0.0 208 | 209 | The first version of Assembly Line. Started at version 2, since this is a continuation of [MAVirtualCourt](https://github.com/SuffolkLITLab/MAVirtualCourt). 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Suffolk Legal Innovation and Technology Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Suffolk LIT Lab Document Assembly Line 2 | 3 | [![PyPI version](https://badge.fury.io/py/docassemble-AssemblyLine.svg)](https://badge.fury.io/py/docassemble-AssemblyLine) 4 | 5 | drawing 6 | 7 | The Assembly Line Project is a collection of volunteers, students, and institutions who joined together 8 | during the COVID-19 pandemic to help increase access to the court system. Our vision is mobile-friendly, 9 | easy to use **guided** online forms that help empower litigants to access the court remotely. 10 | 11 | Our signature project is [CourtFormsOnline.org](https://courtformsonline.org). 12 | 13 | We designed a step-by-step, assembly line style process for automating court forms on top of Docassemble 14 | and built several tools along the way that **you** can use in your home jurisdiction. 15 | 16 | This package contains **runtime code** and **pre-written questions** to support authoring robust, 17 | consistent, and attractive Docassemble interviews that help complete court forms. 18 | 19 | Read more on our [documentation page](https://suffolklitlab.org/docassemble-AssemblyLine-documentation/). 20 | 21 | 22 | # Related repositories 23 | 24 | * https://github.com/SuffolkLitLab/docassemble-ALWeaver 25 | * https://github.com/SuffolkLitLab/docassemble-ALMassachusetts 26 | * https://github.com/SuffolkLitLab/docassemble-MassAccess 27 | * https://github.com/SuffolkLitLab/docassemble-ALThemeTemplate 28 | * https://github.com/SuffolkLitLab/EfileProxyServer 29 | 30 | # Documentation 31 | 32 | https://suffolklitlab.org/docassemble-AssemblyLine-documentation/ 33 | 34 | # Installation 35 | 36 | ## Menu-driven installation 37 | 38 | The recommended installation method is with the [guided installation script](https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/installation). 39 | 40 | ## Manual installation 41 | 42 | Normally you do not need to manually install the Assembly Line. Use the [installation script](https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/installation) 43 | if you can. The instructions below are for your optional reference. 44 | 45 | This package depends on the following configuration changes on your Docassemble server: 46 | 47 | * Ensure that [font-awesome](https://docassemble.org/docs/config.html#default%20icons) is enabled (this may be on by default: 48 | ```yaml 49 | default icons: font awesome 50 | ``` 51 | * Add a [Google API key](https://docassemble.org/docs/config.html#google) that has access to: 52 | * Google Places API 53 | * Google Geocoding API 54 | * Add a [VoiceRSS API key](https://docassemble.org/docs/config.html#voicerss) 55 | * Add a [Twilio API key](https://docassemble.org/docs/config.html#twilio) for SMS support 56 | * Add an email account: [Mailgun](https://docassemble.org/docs/config.html#mailgun%20api) or [SendGrid](https://docassemble.org/docs/config.html#sendgrid%20api) recommended for email support 57 | * To show package update time and to enable the feedback form, add a GitHub Private Access token to your config.yml file, like this: 58 | ```yaml 59 | # Needs access to create new issues on repositories 60 | github issues: 61 | username: "suffolklitlab-issues" 62 | token: "12345" 63 | # Does not need any special access to public repositories 64 | github readonly: 65 | username: "suffolklitlab-issues" 66 | password: "45678" 67 | type: "basic" 68 | ``` 69 | * If you are also using the [Assembly Line Weaver](https://github.com/SuffolkLITLab/docassemble-assemblylinewizard), you may want to set up a [Docassemble API key](https://docassemble.org/docs/api.html#manage_api) in your config.yml file to allow you to install packages automatically, like this: 70 | ```yaml 71 | install packages api key: 12345 72 | ``` 73 | 74 | # Migration 75 | 76 | See [discussion here](https://github.com/SuffolkLITLab/docassemble-AssemblyLine/issues/69) 77 | 78 | 79 | # ALDocument class 80 | 81 | ## Purpose 82 | 83 | The ALDocument class is a small utility library that makes it simpler to use the following features in an interview: 84 | 85 | * Conditional assembly of multiple, optional documents that are triggered in different ways in your interview 86 | * An addendum for PDF files that makes it simple to deal with overflow text 87 | * A customizable download screen that lists the documents in a neat table 88 | * A customizable "send" button that allows the user to email the final forms to a location of their choice 89 | 90 | Here is a small snippet that you can copy and modify that shows how to use the most important features of the ALDocument class. 91 | 92 | ``` 93 | --- 94 | objects: 95 | - CRA_Motion_to_Dismiss_attachment: ALDocument.using(filename="CRA_Motion_to_Dismiss", title="Motion to Dismiss CRA", enabled=True, has_addendum=True, default_overflow_message="[See addendum]") 96 | --- 97 | objects: 98 | - al_user_bundle: ALDocumentBundle.using(elements=[CRA_Motion_to_Dismiss_attachment], title="Forms to download and deliver to court", filename="motion_to_dismiss_CRA.pdf") 99 | - al_court_bundle: ALDocumentBundle.using(elements=[CRA_Motion_to_Dismiss_attachment], title="Forms to download and deliver to court", filename="motion_to_dismiss_CRA.pdf") 100 | --- 101 | generic object: ALDocument 102 | attachment: 103 | variable name: x.addendum 104 | docx template file: docx_addendum.docx 105 | --- 106 | code: | 107 | CRA_Motion_to_Dismiss_attachment.overflow_fields['reasons_for_request'].overflow_trigger = 640 108 | CRA_Motion_to_Dismiss_attachment.overflow_fields['reasons_for_request'].label = "Reasons for request" 109 | CRA_Motion_to_Dismiss_attachment.overflow_fields.gathered = True 110 | 111 | --- 112 | attachment: 113 | variable name: CRA_Motion_to_Dismiss_attachment[i] 114 | name: CRA Motion to Dismiss 115 | filename: CRA_Motion_to_Dismiss 116 | skip undefined: True 117 | pdf template file: CRA_Motion_to_Dismiss.pdf 118 | fields: 119 | - "court_county": ${ trial_court.address.county } 120 | - "docket_number": ${ docket_number } 121 | - "user_signature": ${ users[0].signature_if_final(i) } 122 | - "signature_date": ${ signature_date } 123 | ``` 124 | 125 | It is very common to have a *contingent* document in ALDocument. If your document is contingent, remove the `enabled=True` from the object declaration, and use 126 | some other method to "turn on" the attachment. 127 | 128 | E.g., 129 | 130 | ``` 131 | code: | 132 | CRA_Motion_to_Dismiss_attachment.enabled = condition1 and condition2 133 | ``` 134 | 135 | # Changelog 136 | 137 | See [CHANGELOG.MD](https://github.com/SuffolkLITLab/docassemble-AssemblyLine/blob/main/CHANGELOG.md) 138 | -------------------------------------------------------------------------------- /bumpversion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | helpvar="Setup: 4 | Copy the example env file at '.env.example' to '.env', and fill the values of your own variables 5 | * TEAMS_BUMP_WEBHOOK: a url that you setup following the procudure at https://stackoverflow.com/a/65759554 6 | * TWINE_USERNAME: your Pypi username, or '__token__' if you are using an API token 7 | * TWINE_PASSWORD: your Pypi password, or an API token, including the 'pypi-' prefix 8 | 9 | How to run: 10 | 11 | $ (source .env && ./bumpversion.sh patch) 12 | 13 | Can pass in test, patch, minor, or major depending on which part of the version you're bumping. We 14 | roughly follow semantic versioning: 15 | * test is a nonstandard version increment. Since you have to install Docassemble packages to test 16 | if they work in other packages, you should bump test before installing to see what works 17 | * patch is a version that changes nothing about how other interviews use your package/library. 18 | Usually just bug fixes. 19 | * minor is the addition of a feature, and generally means that you cannot downgrade after upgrading 20 | * major is a breaking change, either internally or externally 21 | For more info about semantic versioning (aka semver), see https://semver.org/ 22 | " 23 | 24 | real_run=true 25 | announce_to_teams=true 26 | publish_to_pypi=true 27 | 28 | while [ -n "$1" ]; do 29 | case "$1" in 30 | -h | --help) 31 | echo -n "$helpvar" 32 | shift 33 | exit 34 | ;; 35 | -n | --dry-run) 36 | real_run=false 37 | shift 38 | ;; 39 | --disable-teams) 40 | announce_to_teams=false 41 | shift 42 | ;; 43 | --disable-pypi) 44 | publish_to_pypi=false 45 | shift 46 | ;; 47 | --) 48 | shift # double dash makes for parameters 49 | break 50 | ;; 51 | major | minor | patch | test) 52 | bump_type=$1 53 | shift 54 | ;; 55 | *) 56 | break # found not an option: just continue 57 | ;; 58 | esac 59 | done 60 | 61 | 62 | echo "$announce_to_teams" 63 | echo "$publish_to_pypi" 64 | echo "$bump_type" 65 | 66 | ### Make sure you have all the necessary commands installed and env vars set 67 | if ! git --help > /dev/null 2>&1; then 68 | echo "You must have the git command installed" 69 | exit 1 70 | fi 71 | 72 | if ! bumpversion --help > /dev/null 2>&1; then 73 | echo "You need to install the bumpversion python library" 74 | exit 1 75 | fi 76 | 77 | if ! twine --help > /dev/null 2>&1; then 78 | echo "You need to install the twine python library to publish to pypi" 79 | exit 1 80 | fi 81 | 82 | if [ -z "$bump_type" ]; then 83 | echo -n "You need to pass in test, patch, minor, or major: $helpvar" 84 | exit 1 85 | fi 86 | 87 | if "$publish_to_pypi" && [ -z "$TWINE_USERNAME" ]; then 88 | echo "You need to have the TWINE_USERNAME environment variable defined" 89 | exit 1 90 | fi 91 | 92 | if "$publish_to_pypi" && [ -z "$TWINE_PASSWORD" ]; then 93 | echo "You need to have the TWINE_PASSWORD environment variable defined" 94 | exit 1 95 | fi 96 | 97 | if "$announce_to_teams" && [ -z "$TEAMS_BUMP_WEBHOOK" ]; then 98 | echo "You need to have the TEAMS_BUMP_WEBHOOK environment variable defined" 99 | exit 1 100 | fi 101 | 102 | # Set after, because we can't iterate through things without getting to the end 103 | set -euo pipefail 104 | 105 | # TODO(brycew): should we restrict this to only work on default branches? 106 | git fetch --all 107 | branch=$(git rev-parse --abbrev-ref HEAD) 108 | if [ -n "$(git ls-remote --exit-code --heads origin "$branch")" ] && \ 109 | [ x"$(git merge-base "$branch" origin/"$branch")" != x"$(git rev-parse origin/"$branch")" ] 110 | then 111 | echo "$branch is behind the origin. Pull from origin first." 112 | exit 1 113 | fi 114 | 115 | ### Makes git commit and tag 116 | if $real_run 117 | then 118 | new_version=$(bumpversion --list --config-file .bumpversion.cfg "$bump_type" | grep new_version | cut -d= -f 2) 119 | else 120 | new_version=$(bumpversion --list --dry-run --verbose --config-file .bumpversion.cfg "$bump_type" | grep new_version | cut -d= -f 2) 121 | fi 122 | 123 | if [ "$bump_type" = "patch" ] || [ "$bump_type" = "minor" ] || [ "$bump_type" = "major" ] 124 | then 125 | echo What has changed about this "$bump_type" version? Press ctrl-d to finish, ctrl-c to cancel 126 | release_update=$( CHANGELOG.md 130 | git add CHANGELOG.md && git commit --amend -C HEAD 131 | fi 132 | fi 133 | 134 | ### Make and update Pypi package 135 | rm -rf build dist ./*.egg-info 136 | python3 setup.py sdist 137 | 138 | ### Auto get project and repo name to put in Teams message 139 | remote_url=$(git remote get-url --push origin ) 140 | repo_name=$(basename -s ".git" "$remote_url") 141 | org_name=$(basename "$(dirname "$remote_url")") 142 | if [[ "$org_name" = *@* ]]; then 143 | # Likely an SSH URL. Split at the ':' 144 | org_name=$(echo "$org_name" | cut -d ':' -f2) 145 | fi 146 | project_name=$(echo "$repo_name" | cut -d - -f2) 147 | 148 | # This JSON was built using: https://messagecardplayground.azurewebsites.net/, 149 | # and https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference 150 | link_ver=${new_version//["."]} 151 | sed -e "s/{{version}}/$new_version/g; s/{{link_version}}/$link_ver/g; s/{{project_name}}/$project_name/g; s/{{org_name}}/$org_name/g; s/{{repo_name}}/$repo_name/g;" << EOF > /tmp/teams_msg_to_send.json 152 | { 153 | "@type": "MessageCard", 154 | "@context": "https://schema.org/extensions", 155 | "summary": "{{project_name}} Version released", 156 | "themeColor": "0078D7", 157 | "title": "{{project_name}} Version {{version}} released", 158 | "sections": [ 159 | { 160 | "activityTitle": "Version {{version}}", 161 | "activityImage": "https://avatars.githubusercontent.com/u/33028765?s=200&v=4", 162 | "facts": [ 163 | { 164 | "name": "Repository:", 165 | "value": "{{org_name}}/{{repo_name}}" 166 | }, 167 | { 168 | "name": "Tag", 169 | "value": "v{{version}}" 170 | } 171 | ], 172 | "text": "" 173 | } 174 | ], 175 | "potentialAction": [ 176 | { 177 | "@type": "OpenUri", 178 | "name": "See Changelog", 179 | "targets": [ 180 | { 181 | "os": "default", 182 | "uri": "https://github.com/{{org_name}}/{{repo_name}}/blob/main/CHANGELOG.md#version-v{{link_version}}" 183 | } 184 | ] 185 | }, 186 | { 187 | "@type": "OpenUri", 188 | "name": "View in GitHub", 189 | "targets": [ 190 | { 191 | "os": "default", 192 | "uri": "http://github.com/{{org_name}}/{{repo_name}}/releases/tag/v{{version}}" 193 | } 194 | ] 195 | } 196 | ] 197 | } 198 | EOF 199 | 200 | if $real_run 201 | then 202 | ## Push and save stuff at the very end of the script 203 | git push 204 | git push --tags 205 | # Only push to pypi and announce on non-test bumps 206 | if [ ! "$bump_type" = "test" ] 207 | then 208 | if "$publish_to_pypi" 209 | then 210 | # Needs TWINE_USERNAME and TWINE_PASSWORD 211 | twine upload --repository 'pypi' dist/* --non-interactive 212 | fi 213 | if "$announce_to_teams" 214 | then 215 | curl -H "Content-Type:application/json" -d "@/tmp/teams_msg_to_send.json" "$TEAMS_BUMP_WEBHOOK" 216 | fi 217 | fi 218 | rm -rf build dist ./*.egg-info 219 | 220 | else 221 | if [ "$bump_type" = "minor" ] || [ "$bump_type" = "major" ] 222 | then 223 | echo "Changelog would be:" 224 | echo -e "# v$new_version\n\n$release_update\n\n$(cat CHANGELOG.md)" 225 | fi 226 | 227 | if [ ! "$bump_type" = "test" ] 228 | then 229 | if "$publish_to_pypi" 230 | then 231 | echo "Would push to Pypi" 232 | else 233 | echo "Would NOT push to Pypi" 234 | fi 235 | if "$announce_to_teams" 236 | then 237 | echo "Teams message JSON is: $(cat /tmp/teams_msg_to_send.json)" 238 | else 239 | echo "Would NOT announce to teams" 240 | fi 241 | else 242 | echo "Would NOT push to Pypi or announce to Teams" 243 | fi 244 | 245 | fi 246 | rm "/tmp/teams_msg_to_send.json" 247 | 248 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '3.4.0' 2 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/custom_jinja_filters.py: -------------------------------------------------------------------------------- 1 | # pre-load 2 | 3 | # Because this has the pre-load preamble, this code runs automatically each time the 4 | # server reloads. you do not need to import this module in your code. 5 | 6 | # See: https://docassemble.org/docs/documents.html#register_jinja_filter 7 | 8 | from typing import Any 9 | from docassemble.base.util import register_jinja_filter, DACatchAll 10 | 11 | 12 | def catchall_options(value: Any, *raw_items: Any) -> DACatchAll: 13 | """Jinja2 filter to support defining options for DACatchAll fields inside a DOCX template. 14 | 15 | This filter takes a list of items, which can be strings, dictionaries, or tuples, 16 | and converts them into a list of tuples containing the code and label for each option. 17 | 18 | The items can be in various formats: 19 | 20 | - String: `"code: label"` 21 | - Dictionary: `{"code": "label"}` 22 | - Tuple: `("code", "label")` 23 | - List of any of the above types 24 | 25 | The resulting list of tuples is assigned to the `_catchall_options` attribute of the 26 | DACatchAll object, which can then be used to populate the options in the catchall field. 27 | 28 | Example usage in a DOCX template: 29 | 30 | ``` 31 | {{ my_catchall_field | catchall_options("code1: label1", "code2: label2") }} 32 | 33 | {{ my_catchall_field_2 | catchall_options({"code1": "label1"}, {"code2": "label2"}) }} 34 | ``` 35 | 36 | Example in an interview with `features: use catchall: True` turned on: 37 | 38 | ``` 39 | --- 40 | if: | 41 | hasattr(x, "_catchall_options") 42 | generic object: DACatchAll 43 | question: | 44 | ${ x.object_name() }? 45 | fields: 46 | - ${ x.object_name() }: x.value 47 | code: x._catchall_options 48 | ``` 49 | 50 | Args: 51 | value (DACatchAll): The DACatchAll object to which the options will be assigned. 52 | *raw_items: A variable number of arguments representing the options to be added. 53 | Returns: 54 | DACatchAll: The modified DACatchAll object with the assigned options. 55 | """ 56 | if isinstance(value, DACatchAll): 57 | pairs = [] 58 | for item in raw_items: 59 | if isinstance(item, str): 60 | if ":" in item: 61 | code, label = item.split(":", 1) 62 | else: 63 | code = label = item 64 | pairs.append((code.strip(), label.strip())) 65 | elif isinstance(item, dict): 66 | for code, label in item.items(): 67 | pairs.append((code.strip(), label.strip())) 68 | elif isinstance(item, tuple) and len(item) == 2: 69 | code, label = item 70 | pairs.append((code.strip(), label.strip())) 71 | elif isinstance(item, list): 72 | for subitem in item: 73 | if isinstance(subitem, str): 74 | if ":" in subitem: 75 | code, label = subitem.split(":", 1) 76 | else: 77 | code = label = subitem 78 | pairs.append((code.strip(), label.strip())) 79 | elif isinstance(subitem, dict): 80 | for code, label in subitem.items(): 81 | pairs.append((code.strip(), label.strip())) 82 | elif isinstance(subitem, tuple) and len(subitem) == 2: 83 | code, label = subitem 84 | pairs.append((code.strip(), label.strip())) 85 | value._catchall_options = pairs 86 | return value 87 | 88 | 89 | def catchall_label(value: Any, label: str) -> DACatchAll: 90 | """Jinja2 filter to allow you to define a label for a DACatchAll field inside a DOCX template. 91 | 92 | This filter takes a label string and assigns it to the `label` attribute of the 93 | DACatchAll object. This label can be used to provide a more descriptive name for the 94 | catchall field in the user interface. 95 | 96 | Example usage in a DOCX template: 97 | ``` 98 | {{ my_catchall_field | catchall_label("My Custom Label") }} 99 | ``` 100 | Example in an interview with `features: use catchall: True` turned on: 101 | ``` 102 | --- 103 | generic object: DACatchAll 104 | question: | 105 | ${ x.label if hasattr(x, "label") else x.object_name() }? 106 | fields: 107 | - ${ x.label if hasattr(x, "label") else x.object_name() }: x.value 108 | ``` 109 | Args: 110 | value (DACatchAll): The DACatchAll object to which the label will be assigned. 111 | label (str): The label string to assign to the DACatchAll object. 112 | 113 | Returns: 114 | DACatchAll: The modified DACatchAll object with the assigned label. 115 | 116 | """ 117 | if isinstance(value, DACatchAll): 118 | value.label = label 119 | return value 120 | 121 | 122 | register_jinja_filter("catchall_options", catchall_options) 123 | register_jinja_filter("catchall_label", catchall_label) 124 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_code.yml: -------------------------------------------------------------------------------- 1 | ############################################# 2 | # Contains code to do various actions that 3 | # belong in YAML and not .py files 4 | # 1. Email file to court TODO: does this really belong here? 5 | # 2. Signature flow 6 | ########################################################## 7 | # Code to handle emailing court 8 | --- 9 | modules: 10 | - .sign 11 | --- 12 | # Placeholder 13 | objects: 14 | - court_emails: DADict.using(auto_gather=False, gathered=True) 15 | ################### Backwards compatibility #################################### 16 | - user_has_saved_answers: DAEmpty # For people who forked the pre-2.11.0 intro screen 17 | --- 18 | code: | 19 | ready_to_email 20 | prevent_going_back() 21 | email_status 22 | form_delivery_complete = True 23 | --- 24 | code: | 25 | court_email = court_emails.get(trial_court.name,None) 26 | if not court_email: 27 | found_email = False 28 | else: 29 | found_email = True 30 | --- 31 | code: | 32 | bcc_failsafe = court_emails.get('Failsafe Address',None) 33 | --- 34 | comment: | 35 | You are encouraged to overwrite these emails during testing! 36 | code: | 37 | dev_email_to_use = "massaccess@suffolk.edu" 38 | dev_bcc_email = "quinten@lemmalegal.com" 39 | --- 40 | need: 41 | - bcc_failsafe 42 | - email_to_court_template 43 | - court_email 44 | - dev_email_to_use 45 | - dev_bcc_email 46 | - should_cc_user 47 | code: | 48 | if will_send_to_real_court(): 49 | email_to_use = court_email 50 | bcc_email = bcc_failsafe 51 | else: 52 | email_to_use = dev_email_to_use 53 | bcc_email = dev_bcc_email 54 | 55 | # temporary for testing 56 | # email_to_use = "massaccess@suffolk.edu" 57 | 58 | if task_not_yet_performed('send email'): 59 | log(f"Court email is {court_email}, sending email to {email_to_use}") 60 | email_success = False 61 | if should_cc_user: 62 | log('Sending email to ' + email_to_use + ' and ccing ' + cc_email + ' for ' + str(users) + ' bcc to ' + bcc_email) 63 | email_success = send_email(to=email_to_use, template=email_to_court_template, task='send email', 64 | cc=cc_email, bcc=bcc_email, attachments=al_final_form_to_file.as_pdf_list(key='final')) 65 | else: 66 | log('Sending email to ' + email_to_use + ' and no cc ' + ' for ' + str(users) + ' bcc to ' + bcc_email) 67 | email_success = send_email(to=email_to_use, template=email_to_court_template, task='send email', 68 | attachments=al_final_form_to_file.as_pdf_list(key='final'), bcc=bcc_email) 69 | mark_task_as_performed('send email') 70 | sent_email_to_court = True 71 | --- 72 | ############################################################## 73 | # new attachment handling code 74 | --- 75 | objects: 76 | - al_cover_page: ALDocument.using(title="Cover page", filename="cover_page", has_addendum=False, enabled=True) 77 | --- 78 | # This is the *default* version of both objects as an example. 79 | # You will ALWAYS want to have your own version of these object blocks 80 | # in your own code. 81 | objects: 82 | - al_court_bundle: ALDocumentBundle.using(filename="court_forms.pdf", title="Forms to download and deliver to court", elements=[]) 83 | - al_user_bundle: ALDocumentBundle.using(filename="court_forms.pdf", title="Forms to download for your own reference", elements=[]) 84 | --- 85 | objects: 86 | - al_final_form_to_file: ALDocumentBundle.using(elements=[al_cover_page, al_court_bundle], filename=al_court_bundle.filename, title=al_court_bundle.title) 87 | --- 88 | sets: al_num_package_pages 89 | code: | 90 | # Gets the number of pages, plus the first cover page 91 | # TODO(brycew): figure out a better way to estimate the number of pages, 92 | # including addendum, without generating a full file 93 | al_num_package_pages = al_court_bundle.as_pdf(key='preview').num_pages() + 1 94 | --- 95 | code: | 96 | # You can define this in the interview's code block. 97 | # Should be a list. 98 | al_download_titles = al_court_bundle.get_titles() 99 | --- 100 | code: | 101 | # This is a fallback package title to avoid errors on new forms 102 | # It defines package_title as the metadata title. 103 | # You may want a better form title for most interviews 104 | package_title = all_variables(special='metadata').get('title','').rstrip() 105 | --- 106 | attachment: 107 | variable name: al_cover_page[i] 108 | docx template file: general_cover_sheet.docx 109 | --- 110 | ################################################################# 111 | # Signature code 112 | --- 113 | # Triggers some questions to ask for signatures on either desktop or mobile 114 | # Allows sending via text/email 115 | id: basic questions signature flow control 116 | code: | 117 | # Three branches: 118 | # 1. Decided to sign on the PC 119 | # 2. Sent someone a link to sign the form 120 | # 3. Used the QR code 121 | saw_signature_choice 122 | # Branch 1: PC 123 | if signature_choice == "sign_after_printing": 124 | for signature in signature_fields: 125 | define(signature, DAEmpty()) 126 | elif signature_choice == "typed_signature": 127 | for signature in signature_fields: 128 | # Signature fields are strings like "users[0].signature". If the string ends in .signature, remove that and replace with .typed_signature, only at the end of the string 129 | if signature.endswith('signature'): 130 | typed_signature = signature[:-9] + 'typed_signature' 131 | define(signature, DAFile(signature, filename="signature.png")) 132 | value(signature).initialize() 133 | create_signature(value(typed_signature), value(signature).path(), signature_prefix = al_typed_signature_prefix, font_name = al_typed_signature_font) 134 | else: 135 | log("Signature field " + signature + " does not end with .signature, not creating typed signature") 136 | elif signature_choice in ['this device', 'this_device']: 137 | for signature in signature_fields: 138 | value(signature) 139 | # Check for Branch 2 or 3 140 | elif signature_choice == 'phone': 141 | saw_signature_qrcode 142 | # User will click next. link_cell will only be defined if they chose the texting option 143 | if showifdef("link_cell") and task_not_yet_performed('send signature link'): 144 | # They used the text option 145 | send_sms(task='send signature link', to=link_cell,template=interview_link) 146 | signature_wait_screen 147 | for signature in signature_fields: 148 | value(signature) 149 | else: # Branch 3: They used the QR Code. No special screen, just continue 150 | for signature in signature_fields: 151 | value(signature) 152 | # Show the follow-up if they sent the link to their phone by QR or by SMS 153 | if device() and (device().is_mobile or device().is_touch_capable): 154 | signature_phone_followup 155 | basic_questions_signature_flow = True 156 | --- 157 | code: | 158 | # Sometimes we have values that can be set from interview_metadata. 159 | # They don't always work, but interview author can override. 160 | 161 | # Some interviews have multiple interview_metadata keys. Look in the 1st one 162 | first_form_key = next(iter(interview_metadata),'') 163 | if isinstance(interview_metadata.get(first_form_key,{}), dict): 164 | first_form = interview_metadata.get(first_form_key,{}) 165 | else: 166 | first_form = {} 167 | del(first_form_key) 168 | --- 169 | depends on: 170 | - user_ask_role 171 | code: | 172 | # DEPRECATED as of 2.17.1. Won't be removed, but you shouldn't reference this 173 | # variable in your code. Use `user_ask_role` instead. 174 | # If the author hasn't explicitly defined it in the interview_order block, 175 | # define user_role to be either "defendant" or "plaintiff", 176 | # either by checking interview_metadata or asking the user. 177 | # The only valid values for user_role are "plaintiff" and "defendant". 178 | 179 | # We will check for the "typical_role" key. 180 | # Some older wizarded code doesn't define `typical_role`, so fall back 181 | # to "unknown" if the key is not present. 182 | # first_form is the first entry in interview_metadata 183 | if not first_form.get("typical_role", 'unknown') == 'unknown': 184 | user_role = first_form.get("typical_role") 185 | else: 186 | # If the user role was set to "unknown" at the time wizard run 187 | user_role = user_ask_role 188 | --- 189 | # HTML for interviews using ALKiln tests at ALKiln 190 | # Necessary for servers that set their `restrict input variables` config value to `True` 191 | # If you are using al_package_unstyled.yml, copy it into your `default screen parts: post` 192 | template: alkiln_trigger_html 193 | content: | 194 | 195 | --- 196 | # HTML for interviews using ALKiln tests at ALKiln versions 5.9.0 and above. 197 | # Necessary for interviews that use docassemble generic objects or index variables 198 | # If you are using al_package_unstyled.yml, copy it into your `default screen parts: post` 199 | template: alkiln_proxy_html 200 | content: | 201 | 207 | --- 208 | code: | 209 | al_sessions_additional_variables_to_filter = [] 210 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_document.yml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | javascript: 4 | - aldocument.js 5 | css: 6 | - aldocument.css 7 | --- 8 | # This must be in Docassemble YAML due to limits in how Docassemble 9 | # searches for events. It also somewhat safer to limit the scope of what 10 | # events can be triggered by JS 11 | generic object: ALDocumentBundle 12 | event: x.send_email_action_event 13 | code: | 14 | email_success = x.send_email( 15 | to=action_argument('email'), 16 | editable=action_argument('editable'), 17 | template=( 18 | value(action_argument('template_name')) 19 | if action_argument('template_name') 20 | else None 21 | ), 22 | key=action_argument('key') 23 | ) 24 | if email_success: 25 | # Exact phrase below is in Docassemble's words dictionary 26 | log(word('E-mail was sent to') + ' ' + action_argument('email') ,'success') 27 | else: 28 | # E-mail failed is not in the default Docassemble words dictionary 29 | log(word('E-mail failed'), 'error') 30 | --- 31 | generic object: ALDocumentBundle 32 | event: x.send_email_to_action_event 33 | code: | 34 | email_success = x.send_email( 35 | to=action_argument('email'), 36 | editable=action_argument('editable'), 37 | template=( 38 | value(action_argument('template_name')) 39 | if action_argument('template_name') 40 | else None 41 | ), 42 | key=action_argument('key'), 43 | ) 44 | if email_success: 45 | log("Email sent to " + action_argument('email')) # to log file 46 | else: 47 | log(word('E-mail failed to ') + action_argument('email')) 48 | 49 | json_response({'success': email_success}) 50 | 51 | 52 | 53 | --- 54 | generic object: ALDocumentBundle 55 | code: | 56 | # Used in the email subject line when email is triggered on download screen. 57 | # This defaults to the interview title. For example, if the 58 | # interview title is "Guardianship Helper", the email 59 | # subject would be "Your Guardianship Helper document from CourtFormsOnline is ready". 60 | # Customize this if you prefer something like "Your guardianship document is ready" 61 | x.document_label = str(all_variables(special='titles').get('title', all_variables(special='metadata').get('title', 'assembled'))).strip() 62 | --- 63 | generic object: ALDocumentBundle 64 | template: x.send_email_template 65 | subject: | 66 | % if len(x) > 1: 67 | Your ${ x.document_label } documents from ${ al_app_name } are ready 68 | % else: 69 | Your ${ x.document_label } document from ${ al_app_name } is ready 70 | % endif 71 | content: | 72 | Your ${ x.document_label } document is attached. 73 | 74 | Visit ${ AL_ORGANIZATION_HOMEPAGE } to learn more. 75 | --- 76 | generic object: ALDocumentBundle 77 | template: x.get_email_copy 78 | content: | 79 | Get a copy of the documents in email 80 | --- 81 | generic object: ALDocumentBundle 82 | template: x.include_editable_documents 83 | content: | 84 | Include an editable copy 85 | --- 86 | generic object: ALDocumentBundle 87 | template: x.zip_label 88 | content: | 89 | Download all 90 | --- 91 | generic object: ALDocumentBundle 92 | template: x.full_pdf_label 93 | content: | 94 | Download as one PDF 95 | --- 96 | id: al exhibit ocr pages bg 97 | event: al_exhibit_ocr_pages 98 | code: | 99 | to_pdf = action_argument('to_pdf') 100 | from_file = action_argument('from_file') 101 | background_response(ocrmypdf_task(from_file, to_pdf)) 102 | --- 103 | ############## Background document assembly ################# 104 | --- 105 | generic object: ALDocumentBundle 106 | code: | 107 | x.generate_downloads_task = background_action(x.attr_name('create_downloads')) 108 | --- 109 | generic object: ALDocumentBundle 110 | event: x.create_downloads 111 | code: | 112 | download_response = x.get_cacheable_documents(key="final", pdf=True, include_full_pdf=True) 113 | background_response_action(x.attr_name('save_downloads'), download_response=download_response) 114 | --- 115 | generic object: ALDocumentBundle 116 | code: | 117 | x.generate_downloads_with_docx_task = background_action(x.attr_name('create_downloads_with_docx')) 118 | --- 119 | generic object: ALDocumentBundle 120 | event: x.create_downloads_with_docx 121 | code: | 122 | download_response = x.get_cacheable_documents(key="final", pdf=True, docx=True, include_full_pdf=True) 123 | background_response_action(x.attr_name('save_downloads'), download_response=download_response) 124 | --- 125 | generic object: ALDocumentBundle 126 | event: x.save_downloads 127 | code: | 128 | x._downloadable_files = action_argument('download_response') 129 | 130 | background_response() 131 | --- 132 | id: waiting screen 133 | question: | 134 | Please wait while we make your documents 135 | subquestion: | 136 | This can take a few minutes. 137 | 138 |
139 | Making documents... 140 |
141 | event: al_download_waiting_screen 142 | reload: True 143 | --- 144 | ########## Background preview assembly ################ 145 | --- 146 | generic object: ALDocumentBundle 147 | code: | 148 | x.generate_preview_task = background_action(x.attr_name('generate_preview_event')) 149 | --- 150 | generic object: ALDocumentBundle 151 | event: x.generate_preview_event 152 | code: | 153 | preview_response = x.as_pdf(key="preview") 154 | background_response_action(x.attr_name('save_preview'), preview_response=preview_response) 155 | --- 156 | generic object: ALDocumentBundle 157 | event: x.save_preview 158 | code: | 159 | x._preview_file = action_argument('preview_response') 160 | background_response() 161 | --- 162 | id: preview waiting screen 163 | question: | 164 | Please wait while we generate a preview of your document 165 | subquestion: | 166 | This can take a few minutes. 167 | 168 |
169 | Generating preview... 170 |
171 | event: al_preview_waiting_screen 172 | reload: True -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_language.yml: -------------------------------------------------------------------------------- 1 | --- 2 | modules: 3 | - .language 4 | --- 5 | translations: 6 | - translation_es.xlsx 7 | - translation_ht.xlsx 8 | - translation_pt.xlsx 9 | --- 10 | code: | 11 | enable_al_language = True 12 | --- 13 | code: | 14 | al_user_default_language = "en" 15 | --- 16 | code: | 17 | al_interview_languages = [al_user_default_language] 18 | --- 19 | default screen parts: 20 | navigation bar html: | 21 | % if enable_al_language and len(al_interview_languages) > 1: 22 | ${ get_language_list_dropdown(al_interview_languages,current=al_user_language ) } 23 | % endif 24 | --- 25 | initial: True 26 | code: | 27 | if enable_al_language: 28 | set_language(al_user_language) 29 | --- 30 | code: | 31 | if enable_al_language: 32 | if url_args.get('lang'): 33 | al_user_language = url_args.get('lang') 34 | else: 35 | al_user_language = al_user_default_language 36 | --- 37 | event: al_change_language 38 | code: | 39 | if 'lang' in action_arguments(): 40 | al_user_language = action_argument('lang') 41 | set_language(al_user_language) -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_package.yml: -------------------------------------------------------------------------------- 1 | # Deprecated; use assembly_line.yml instead 2 | --- 3 | include: 4 | - assembly_line.yml -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_package_unstyled.yml: -------------------------------------------------------------------------------- 1 | # DEPRECATED - use assembly_line_unstyled instead 2 | --- 3 | include: 4 | - assembly_line_unstyled.yml -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_question_test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ################ What is this? ############### 3 | # This file is a small playground to demo new features. 4 | # If you are building a new feature, it should also have an accompanying 5 | # test. This file is not a test, just a demo to assist PRs and will change 6 | # frequently as new features are added. 7 | --- 8 | include: 9 | - al_package.yml 10 | --- 11 | code: | 12 | AL_DEFAULT_COUNTRY = "US" 13 | --- 14 | code: | 15 | AL_DEFAULT_STATE = "MA" 16 | --- 17 | code: | 18 | interview_short_title = "AL Question Test" 19 | --- 20 | metadata: 21 | title: | 22 | AL Question Test 23 | authors: 24 | - Quinten Steenhuis 25 | - Plocket 26 | - Lily 27 | - Bryce Willey 28 | - Caroline Robinson 29 | - David Colarusso 30 | - Kate Barry 31 | --- 32 | modules: 33 | - docassemble.base.util 34 | - docassemble.base.core 35 | - docassemble.base.functions 36 | --- 37 | objects: 38 | - chiropractors: ALPeopleList.using(target_number=1, ask_number=True) 39 | - children: ALPeopleList.using(ask_number=True, complete_attribute="complete") 40 | --- 41 | mandatory: True 42 | code: | 43 | al_intro_screen 44 | test_label_functions 45 | feature_tests_1 46 | al_user_bundle 47 | chiropractors.gather() 48 | other_parties.gather() 49 | users.gather() 50 | users[0].gender 51 | users[0].address.address 52 | users[0].mailing_address.address 53 | users[0].phone_number 54 | trial_court 55 | big_text_field 56 | children.gather() 57 | preview_screen 58 | basic_questions_signature_flow 59 | many_bundles_test 60 | end 61 | --- 62 | question: | 63 | Test 64 | fields: 65 | - code: | 66 | users[0].name_fields() 67 | - code: | 68 | users[0].gender_fields() 69 | - code: | 70 | users[0].address_fields(country_code=AL_DEFAULT_COUNTRY, default_state=AL_DEFAULT_STATE) 71 | continue button field: test_label_functions 72 | --- 73 | code: | 74 | children[i].name.first 75 | children[i].birthdate 76 | children[i].complete=True 77 | --- 78 | # Page to test certain features 79 | # Reduced text for ease of testing custom audio element 80 | # Currently testing: 81 | # - Custom audio element reading behavior is correct 82 | # - Custom audio element reading behavior is correct 83 | id: test behavior of some features 84 | continue button field: feature_tests_1 85 | question: | 86 | Test custom audio element behavior on help button 87 | help: | 88 | Show help screen 89 | --- 90 | id: preview 91 | continue button field: preview_screen 92 | question: | 93 | The preview copy of your form is ready 94 | subquestion: | 95 | Take a look at the files below. When you are ready, click "next" 96 | to continue and add your signature. 97 | 98 | ${ al_user_bundle.as_pdf(key='preview') } 99 | 100 | [Edit mako_template status](${ url_action('mako_template_enabled') }) 101 | 102 | [Edit user's name](${url_action('users[0].name.first')}) 103 | --- 104 | question: | 105 | Try a big text field 106 | subquestion: | 107 | Try more than 200 characters to trigger addendum. 108 | fields: 109 | - Write something: big_text_field 110 | datatype: area 111 | --- 112 | objects: 113 | # Mako template will use the generic addendum 114 | - mako_template: ALDocument.using(title="Mako template", filename="mako_template", has_addendum=True, default_overflow_message=" [See addendum]") 115 | # `has_addendum` should be taken care of by a default attr 116 | - word_template: ALDocument.using(title="Word Template", filename="word_template", enabled=True) 117 | - exhibit_attachment: ALExhibitDocument.using(title="Exhibits", filename="exhibits" ) 118 | --- 119 | code: | 120 | exhibit_attachment.enabled = len(exhibit_attachment.exhibits) > 0 121 | --- 122 | id: Mako template 123 | question: | 124 | Enable Mako template? 125 | yesno: mako_template_enabled 126 | --- 127 | code: | 128 | # You must use an intermediate value to set the enabled status, because it is not 129 | # cached longer than one page load. You do not need to explicitly reconsider it. 130 | mako_template.enabled = mako_template_enabled 131 | --- 132 | objects: 133 | - al_user_bundle: ALDocumentBundle.using(elements=[mako_template, word_template, static_template, exhibit_attachment], filename="user_bundle.pdf", title="All forms to download for your records") 134 | - al_court_bundle: ALDocumentBundle.using(elements=[mako_template, word_template], filename='court_bundle.pdf', title='All forms to send to the court') 135 | - al_single_doc_bundle: ALDocumentBundle.using(elements=[mako_template], filename="one_file_bundle.pdf", title="One file") 136 | --- 137 | code: | 138 | # This is used in the subject of the email template when someone 139 | # emails from ending screen. 140 | metadata_title = "AssemblyLine Question Test" 141 | --- 142 | code: | 143 | mako_template.overflow_fields['big_text_field'].overflow_trigger = 200 144 | mako_template.overflow_fields['children'].overflow_trigger = 2 145 | mako_template.overflow_fields.gathered = True 146 | --- 147 | attachment: 148 | variable name: mako_template[i] 149 | content: | 150 | # Mako template 151 | 152 | You live at ${ users[0].address.on_one_line() } 153 | 154 | Your phone number(s) are: ${ users[0].phone_numbers() }. 155 | 156 | You can be reached by ${ users[0].contact_methods() }. 157 | 158 | You have ${ len(children) } children, named ${ children.familiar() } 159 | 160 | Here is the safe value of "big_text_field": 161 | 162 | > ${ mako_template.safe_value('big_text_field') } 163 | 164 | % if len(children): 165 | Here is some information about your first 2 children: 166 | 167 | Name | Age 168 | -----|---- 169 | % for child in mako_template.safe_value('children'): 170 | ${ child } | ${ child.age_in_years() } 171 | % endfor 172 | % endif 173 | 174 | Signed, 175 | % if i == 'final': 176 | ${ users[0].signature } 177 | % else: 178 | [Sign here] 179 | % endif 180 | --- 181 | attachment: 182 | variable name: word_template[i] 183 | docx template file: sample_word_template.docx 184 | filename: sample_word_template.docx 185 | --- 186 | objects: 187 | - static_template: ALStaticDocument.using(title="Test Static Document", filename="lt1-pullout-10-getting-organized.pdf", enabled=True) 188 | --- 189 | id: end if we need it 190 | event: end 191 | question: Great job! 192 | --- 193 | need: al_user_bundle 194 | id: one enabled doc 195 | continue button field: one_enabled_doc_test 196 | question: | 197 | One enabled file 198 | subquestion: | 199 | ${ al_user_bundle.download_list_html(format="docx") } 200 | --- 201 | need: al_user_bundle 202 | id: download test end screen 203 | continue button field: many_bundles_test 204 | question: | 205 | Your files are ready to download below 206 | subquestion: | 207 | --- 208 | 209 | **Standard Table, view buttons are hidden** 210 | 211 | ${ al_user_bundle.download_list_html(view=False) } 212 | 213 | ${ al_user_bundle.send_button_html(show_editable_checkbox=False) } 214 | 215 | ${ al_user_bundle.as_pdf() } 216 | 217 | --- 218 | 219 | **No zip, docx format** 220 | 221 | ${ al_user_bundle.download_list_html( include_zip=False, format="docx") } 222 | 223 | --- 224 | 225 | **Single file** 226 | 227 | ${ al_single_doc_bundle.download_list_html() } 228 | 229 | ${ al_single_doc_bundle.send_button_html() } -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_reminders.yml: -------------------------------------------------------------------------------- 1 | --- 2 | comment: | 3 | To use this reminder system, include `docassemble.AssemblyLine:al_reminders.yml` in your interview and define a dictionary 4 | named `al_reminders` with these keys: 5 | 6 | * A key for each reminder, which is itself a dictionary with: 7 | * `description`: description of the reminder's purpose 8 | * `date`: date the reminder should be sent. it is calculated only once 9 | * `email template`: name of a docassemble template block 10 | * `sms template`: name of a docassemble template block 11 | 12 | 13 | A complete example is contained below: 14 | --- 15 | variable name: al_reminders 16 | use objects: True 17 | data: 18 | filing_documents: 19 | description: Filing these documents 20 | date: ${ today().plus(days=3).format("yyyy-MM-dd") } 21 | email template: al_reminder_filing_template 22 | sms template: al_reminder_filing_template 23 | 24 | You will want to provide a unique Docassemble template block to define an email and SMS 25 | template for each item you want to remind the user about as well. 26 | 27 | You must also add `al_user_wants_reminders` and `al_reminders_sent_initial_test_message` at an appropriate 28 | place in your interview order or main order block. 29 | 30 | The remaining questions and templates in this YAML file should provide a good default 31 | that works with most interviews. But you can customize every part of the reminder system 32 | independently. 33 | 34 | You may optionally want to copy in and customize the blocks that define: 35 | * al_reminder_initial_email_template (email/SMS the user gets to let them know they are signed up for reminders) 36 | * cron_daily (if you want tasks to run each day that are not related to reminders) 37 | * al_reminders_evaluate_stop_cron (if you want cron to keep running after all reminders are sent. Note this include hourly, daily, weekly and monthly cron) 38 | * al_user_wants_reminders (if you want to customize the question that asks the user to say what reminders they want to get) 39 | 40 | Note that when `cron_daily` runs it changes the last modified date of a session. In turn this means that the session 41 | will not get automatically deleted. So it is important to have logic that stops evaluating cron at an appropriate time 42 | if you want to respect user privacy. You do not need to worry about this if you use the default `al_reminders_evaluate_stop_cron` 43 | code block. 44 | --- 45 | mandatory: True 46 | code: | 47 | allow_cron = True 48 | --- 49 | id: wants reminders 50 | question: | 51 | Would you like to get a reminder about important follow-up steps? 52 | subquestion: | 53 | % if len(al_reminders) == 1: 54 | We can send you a reminder, by text or email, 55 | about ${ next(iter(al_reminders.values()))["description"][:1].lower() }${ fix_punctuation(next(iter(al_reminders.values()))["description"][1:]) } 56 | % else: 57 | We can send you a reminder, by text or email, about: 58 | 59 | % for reminder in al_reminders.values(): 60 | * ${ reminder["description"]} 61 | % endfor 62 | % endif 63 | fields: 64 | - I want to get reminders: al_user_wants_reminders 65 | datatype: yesnoradio 66 | - I want to get reminders by: al_user_preferred_reminder_formats 67 | datatype: checkboxes 68 | code: | 69 | al_reminder_delivery_options 70 | minlength: 1 71 | validation messages: 72 | minlength: | 73 | You need to choose to get reminders by either email or SMS, or both. 74 | show if: al_user_wants_reminders 75 | - Email: al_user_reminder_email 76 | datatype: email 77 | default: | 78 | % if defined("users[0].email"): 79 | ${ users[0].email } 80 | % elif user_logged_in(): 81 | ${ user_info().email } 82 | % endif 83 | show if: al_user_preferred_reminder_formats["email"] 84 | - Mobile phone number: al_user_reminder_phone 85 | default: | 86 | % if defined("users[0].mobile_number"): 87 | ${ users[0].mobile_number } 88 | % elif defined("users[0].phone_number"): 89 | ${ users[0].phone_number } 90 | % endif 91 | validate: | 92 | lambda y: phone_number_is_valid(y) or validation_error("Enter a valid phone number") 93 | show if: al_user_preferred_reminder_formats["sms"] 94 | --- 95 | code: | 96 | al_reminder_delivery_options = [ 97 | {"email": word("Email")}, 98 | ] 99 | if is_sms_enabled(): 100 | al_reminder_delivery_options.append({"sms": word("SMS (text message)")}) 101 | --- 102 | variable name: al_reminders 103 | use objects: True 104 | data: 105 | filing_documents: 106 | description: Filing these documents 107 | date: ${ today().plus(days=3).format("yyyy-MM-dd") } 108 | email template: al_reminder_filing_template 109 | sms template: al_reminder_filing_template 110 | --- 111 | code: | 112 | al_reminders.initial_sms_template = "al_reminder_initial_sms_template" 113 | al_reminders.initial_email_template = "al_reminder_initial_email_template" 114 | --- 115 | template: al_reminder_initial_sms_template 116 | content: | 117 | You asked to get a reminder of important dates when you used "${ all_variables(special='metadata').get('title', AL_ORGANIZATION_TITLE) }". 118 | You will get the reminders at this phone number. 119 | 120 | Click to unsubscribe ${ interview_url_action('al_reminders_unsubscribe_emails') } to stop reminders from this 121 | interview or reply STOP if you no longer want any reminders from ${ AL_ORGANIZATION_TITLE }. 122 | --- 123 | template: al_reminder_initial_email_template 124 | subject: | 125 | You are now signed up for reminders 126 | content: | 127 | You asked to get a reminder of important dates when you used "${ all_variables(special='metadata').get('title', AL_ORGANIZATION_TITLE) }". 128 | You'll get the reminders using this email address. 129 | 130 | You will get about ${ len(al_reminders) + 1 } emails in total (including this one). 131 | 132 | You can [unsubscribe](${ interview_url_action('al_reminders_unsubscribe_emails') }) if you no longer want to get reminders. 133 | --- 134 | event: al_reminders_unsubscribe_emails 135 | code: | 136 | al_user_wants_reminders = False 137 | al_reminders_display_unsubscribe_success 138 | --- 139 | id: unsubscribe success 140 | event: al_reminders_display_unsubscribe_success 141 | question: | 142 | You are now unsubscribed from reminders from "${ all_variables(special='metadata').get('title', AL_ORGANIZATION_TITLE) }" 143 | subquestion: | 144 | You will no longer get any reminders of important dates by email or text message about this interview. 145 | 146 | [Re-subscribe](${ url_action('al_reminders_resubscribe_emails') }) 147 | back button: False 148 | --- 149 | event: al_reminders_resubscribe_emails 150 | code: | 151 | al_user_wants_reminders = True 152 | al_reminders_display_resubscribe_success 153 | --- 154 | id: resubscribe success 155 | event: al_reminders_display_resubscribe_success 156 | question: | 157 | You are now subscribed to reminders from "${ all_variables(special='metadata').get('title', AL_ORGANIZATION_TITLE) }" 158 | subquestion: | 159 | You will continue to get any reminders of important dates by email or text message that you 160 | have not already been sent. 161 | back button: False 162 | --- 163 | template: al_reminder_filing_template 164 | subject: | 165 | Did you file your ${ all_variables(special='metadata').get('title', AL_ORGANIZATION_TITLE) } documents yet? 166 | content: | 167 | If you haven't filed yet, make a plan or do it today! 168 | --- 169 | event: cron_daily 170 | code: | 171 | # NOTE: there can only be one cron_daily per interview. If you want to add additional daily cron tasks instead of replacing 172 | # this one, you need to copy this block's contents into your new block. 173 | # In addition, this cron will stop ALL cron tasks on the session once it runs through to the end. If you have other 174 | # crons that still need to run, you need to replace this block as well. 175 | 176 | reconsider("al_reminders_cron_daily") 177 | 178 | # Evaluate whether all of the tasks in al_reminders_cron_daily are finished. If so, stop running 179 | # cron so that last modified date of session won't keep refreshing and sessions can be automatically 180 | # cleaned up 181 | reconsider("al_reminders_evaluate_stop_cron") 182 | 183 | # Every cron event should end with response() 184 | response() 185 | --- 186 | only sets: al_reminders_cron_daily 187 | code: | 188 | if al_user_wants_reminders: 189 | import time 190 | from random import random 191 | # Avoid all of the interviews using up resources at the same time, vary the start time a bit 192 | time.sleep(random()*10) 193 | log("Running cron") 194 | for reminder in al_reminders: 195 | if today() >= as_datetime(al_reminders[reminder]["date"]) and task_not_yet_performed(f"al_reminder_{reminder}"): 196 | if al_user_preferred_reminder_formats.get("email") and task_not_yet_performed(f"al_reminder_{reminder}_email"): 197 | if not send_email(to=al_user_reminder_email, template=value(al_reminders[reminder]["email template"]), task=f"al_reminder_{reminder}_email"): 198 | log(f"{ all_variables(special='metadata').get('title', AL_ORGANIZATION_TITLE) }: Couldn't send reminder to { showifdef('users[0]') } via {al_user_reminder_email}") 199 | if al_user_preferred_reminder_formats.get("sms") and task_not_yet_performed(f"al_reminder_{reminder}_sms"): 200 | if not send_sms(to=al_user_reminder_phone, template=value(al_reminders[reminder]["sms template"]), task=f"al_reminder_{reminder}_sms"): 201 | log(f"{ all_variables(special='metadata').get('title', AL_ORGANIZATION_TITLE) }: Couldn't send reminder to { showifdef('users[0]') } via {al_user_reminder_phone}") 202 | mark_task_as_performed(f"al_reminder_{reminder}") 203 | al_reminders_cron_daily = True 204 | --- 205 | only sets: al_reminders_evaluate_stop_cron 206 | code: | 207 | if al_user_wants_reminders: 208 | if all( 209 | task_performed(f"al_reminder_{reminder}") 210 | for reminder in al_reminders 211 | ): 212 | allow_cron = False # Stop calling cron on this session once all tasks performed 213 | else: 214 | allow_cron = False 215 | al_reminders_evaluate_stop_cron = True 216 | --- 217 | only sets: al_reminders_sent_initial_test_message 218 | code: | 219 | log("Sending confirmation message") 220 | if al_user_wants_reminders and task_not_yet_performed(f"al_reminder_initial_reminder"): 221 | if al_user_preferred_reminder_formats.get("email") and task_not_yet_performed(f"al_reminder_initial_reminder_email"): 222 | if not send_email(to=al_user_reminder_email, template=value(al_reminders.initial_email_template), task=f"al_reminder_initial_reminder_email"): 223 | log(f"{ all_variables(special='metadata').get('title', AL_ORGANIZATION_TITLE) }: Couldn't send reminder to { showifdef('users[0]') } via {al_user_reminder_email}") 224 | if al_user_preferred_reminder_formats.get("sms") and task_not_yet_performed(f"al_reminder_initial_reminder_sms"): 225 | if not send_sms(to=al_user_reminder_phone, template=value(al_reminders.initial_sms_template), task=f"al_reminder_initial_reminder_sms"): 226 | log(f"{ all_variables(special='metadata').get('title', AL_ORGANIZATION_TITLE) }: Couldn't send reminder to { showifdef('users[0]') } via {al_user_reminder_phone}") 227 | mark_task_as_performed(f"al_reminder_initial_reminder") 228 | 229 | al_reminders_sent_initial_test_message = True -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_saved_sessions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | code: | 3 | # HACK 4 | # Create a placeholder value to avoid playground errors 5 | al_sessions_snapshot_results = DAEmpty() 6 | --- 7 | initial: True 8 | code: | 9 | ########### Save title and progress to session list ################## 10 | if get_config("assembly line",{}).get("update session metadata", True): 11 | al_temp_metadata = get_interview_metadata( 12 | current_context().filename, current_context().session 13 | ) 14 | al_temp_metadata["auto_title"] = str(al_sessions_interview_title) 15 | al_temp_metadata["progress"] = get_progress() 16 | set_current_session_metadata(al_temp_metadata) 17 | del(al_temp_metadata) 18 | --- 19 | template: al_sessions_interview_title 20 | content: | 21 | % if defined('users[0].name.first'): 22 | ${ comma_and_list(users.complete_elements()) } 23 | % if defined('other_parties[0].name.first'): 24 | v. ${ comma_and_list(other_parties.complete_elements()) } 25 | % endif 26 | % else: 27 | ${ all_variables(special='titles').get('full') } 28 | % if all_variables(special='titles').get('sub'): 29 | : ${ all_variables(special='titles').get('sub') } 30 | % endif 31 | % endif 32 | --- 33 | modules: 34 | - .sessions 35 | --- 36 | modules: 37 | - docassemble.ALToolbox.misc 38 | --- 39 | features: 40 | css: docassemble.ALToolbox:collapse_template.css 41 | --- 42 | id: save answer snapshot 43 | # Save a copy 44 | decoration: save 45 | question: | 46 | Save an answer set 47 | subquestion: | 48 | % if not user_logged_in(): 49 | 55 | 56 | The "answer set" feature lets you make a copy of your answers to 57 | use again and again. You can load this answer set in a new interview 58 | later. 59 | 60 | ${ action_button_html(url_of('login'), label="Sign in", icon="sign-in-alt", size="md", color="primary") } 61 | % else: 62 | 63 | 71 | 72 | 73 | 74 |
75 |

76 | The "answer set" feature lets you make a copy of your answers to 77 | use again and again. You can load this answer set in a new interview later. 78 |

79 |

80 | This feature is designed to help people who work on multiple forms that have 81 | shared questions, such as names, addresses, and information about 82 | family members. 83 |

84 |

85 | You will only be able to copy some of your answers: 86 |

87 |
    88 |
  • Copying answers depends on the way each form was designed. The author may 89 | not have written both forms in a way that is compatible with this feature. 90 |
  • 91 |
  • Images, file uploads, and signatures are never copied by this feature. You 92 | will need to sign the document again and upload any exhibits again. 93 |
  • 94 |
95 |

96 | It may not be possible to edit some answers: 97 |

98 |
    99 |
  • 100 | Editing depends on features in each form. Some forms may not have edit 101 | features that cover all of your answers. 102 |
  • 103 |
104 | 105 |
106 | 107 | 108 | #### Your progress 109 | You visited ${ all_variables(include_internal=True).get('_internal',{}).get('steps',1) } 110 | screens in "${ all_variables(special='titles').get('full') }" so far. 111 | 112 | % endif 113 | fields: 114 | - Name your answer set: al_sessions_snapshot_label 115 | default: | 116 | ${ al_sessions_interview_title } 117 | help: | 118 | Give this answer set a name you can remember later 119 | continue button label: | 120 | :save: Save answer set 121 | script: | 122 | 128 | --- 129 | event: al_sessions_delete_session 130 | code: | 131 | interview_list(action="delete", filename=action_argument("filename"), session=action_argument("session")) 132 | log("Answer set deleted", "success") 133 | --- 134 | id: load answer 135 | decoration: folder-open 136 | question: | 137 | Load answer set 138 | subquestion: | 139 | % if not user_logged_in(): 140 | 146 | 147 | The "answer set" feature lets you make a copy of your answers to 148 | use again and again. You can load the answer set in a new interview later. 149 | 150 | ${ action_button_html(url_of('login'), label="Sign in", icon="sign-in-alt", size="md", color="primary") } 151 | % else: 152 | % if not interview_list_html(): 153 | You haven't created any answer sets yet. 154 | 155 | You can 156 | visit in-progress forms 157 | to keep working on a form that you started earlier. To create an answer set, use the 158 | "Save answer set" link in the main menu. 159 | % else: 160 | Tap the name of an answer set below to re-use the answers in the form you are working 161 | on now. You can 162 | visit in-progress forms 163 | to keep working on a form that you started earlier. 164 | 165 | ${ interview_list_html() } 166 | % endif 167 | % endif 168 | event: al_load_saved_session_screen 169 | back button label: Back 170 | --- 171 | #reconsider: 172 | # - al_load_saved_session_screen 173 | event: al_load_saved_session 174 | code: | 175 | # Using this code block that then loads a screen is critical to get this to work 176 | # without taking people back to the screen after they load the answers 177 | # set_save_status('ignore') 178 | al_load_saved_session_screen 179 | --- 180 | event: al_sessions_fast_forward_session 181 | code: | 182 | # set_save_status('ignore') 183 | al_sessions_snapshot_results = load_interview_answers( 184 | action_argument("i"), 185 | action_argument("session"), 186 | new_session=False, 187 | additional_variables_to_filter=al_sessions_additional_variables_to_filter) 188 | force_ask('al_sessions_load_status') 189 | --- 190 | code: | 191 | if user_logged_in(): 192 | new_session_id = save_interview_answers( 193 | metadata = { "title": al_sessions_snapshot_label }, 194 | additional_variables_to_filter=al_sessions_additional_variables_to_filter, 195 | ) 196 | log(f"Saved interview {al_sessions_snapshot_label} with id {new_session_id}") 197 | else: 198 | log(f"Anonymous user tried to save session snapshot, ignoring.") 199 | al_sessions_save_session_snapshot = True 200 | --- 201 | id: al sessions save status 202 | continue button field: al_sessions_save_status 203 | question: | 204 | % if showifdef('new_session_id'): 205 | Your answer set was successfully saved 206 | % else: 207 | Something went wrong when we tried to save your answer set. 208 | You can try again. 209 | % endif 210 | subquestion: | 211 | Tap "next" to keep answering any unanswered questions and finish the interview. 212 | back button: False 213 | --- 214 | id: al sessions load status 215 | continue button field: al_sessions_load_status 216 | comment: | 217 | #TODO There's no error handling yet so this might be a lie 218 | question: | 219 | % if al_sessions_snapshot_results: 220 | Your answer set was loaded 221 | % else: 222 | Your answer set was not loaded. You can try again. 223 | % endif 224 | subquestion: | 225 | Tap "next" to keep answering any unanswered questions and finish the interview. 226 | back button: False 227 | --- 228 | question: | 229 | Upload a JSON file 230 | subquestion: | 231 | You can upload a file containing an answer set that you exported from this or a different server. 232 | fields: 233 | - Upload a JSON file: al_sessions_json_file 234 | datatype: file 235 | accept: | 236 | "application/json, text/json, text/*, .json" 237 | validation code: | 238 | try: 239 | json.loads(al_sessions_json_file.slurp()) 240 | except: 241 | validation_error("Upload a file with valid JSON") 242 | --- 243 | code: | 244 | al_sessions_snapshot_results = load_interview_json(al_sessions_json_file.slurp()) 245 | al_sessions_import_json = True -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_saved_sessions_store.yml: -------------------------------------------------------------------------------- 1 | --- 2 | metadata: 3 | title: | 4 | Saved answer set 5 | description: | 6 | An interview that is the target for saving and loading answer sets. 7 | 8 | Note: answer sets are not encrypted by default. 9 | tags: 10 | - Answer_set 11 | --- 12 | modules: 13 | - docassemble.ALToolbox.misc 14 | - .sessions 15 | --- 16 | mandatory: True 17 | code: | 18 | multi_user = True 19 | --- 20 | features: 21 | css: 22 | - docassemble.ALToolbox:collapse_template.css 23 | question help button: True 24 | navigation back button: False 25 | --- 26 | mandatory: True 27 | question: | 28 | Saved answer set 29 | % if all_variables(special='titles').get('sub'): 30 | : ${ all_variables(special='titles').get('sub') } 31 | % endif 32 | ${ action_button_html(url_ask(["al_sessions_snapshot_new_label", {"recompute": ["al_sessions_rename_session"]}]), icon="edit", label="Rename", color="secondary") } 33 | subquestion: | 34 | 35 |   |   36 | -------|--------- 37 | User or client | ${ bold(users.complete_elements()) if defined('users') else ''} 38 | Opposing party | ${ bold(other_parties.complete_elements()) if defined('other_parties') else '' } 39 | Page | ${ get_interview_metadata(current_context().filename, current_context().session).get('steps') } 40 | Answered questions | ${ get_interview_metadata(current_context().filename, current_context().session).get('answer_count') } 41 | Export | Export in JSON format 42 | % if get_config('debug') or user_has_privilege('admin', 'developer'): 43 | Developer information | ${ collapse_template(preview_variables) } 44 | % endif 45 | 46 | 47 | You can re-use the answers in this answer set by visiting a new form and selecting 48 | "Load answer set" from the main menu. 49 | 50 | ${ action_button_html(interview_url(i=f"{current_context().current_package}:data/questions/interview_list.yml", label="List in progress forms", color="link", size="md")) } ${ action_button_html(get_config("assembly line",{}).get("new form url", url_of("dispatch")), label="List available forms", icon="list", color="primary", size="md") } 51 | --- 52 | id: rename answer 53 | question: | 54 | Rename answer set 55 | fields: 56 | - New name: al_sessions_snapshot_new_label 57 | default: | 58 | ${ all_variables(special='titles').get('sub') } 59 | continue button label: | 60 | :save: Save changes 61 | --- 62 | code: | 63 | rename_interview_answers(current_context().filename, current_context().session, new_name = al_sessions_snapshot_new_label) 64 | log("New name saved", "success") 65 | al_sessions_rename_session = True 66 | --- 67 | template: preview_variables 68 | subject: | 69 | Preview variables and values 70 | content: | 71 |
72 |   ${ verbatim(json.dumps(all_variables(), sort_keys=True, indent=4)) }
73 |   
-------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_settings.yml: -------------------------------------------------------------------------------- 1 | ########################## 2 | # This file mostly holds placeholder values that will be overwritten 3 | # by individual interviews. Overriding is as simple as copying, altering 4 | # and writing in the value that you want. 5 | --- 6 | code: | 7 | # Set this to the name of your application 8 | # Defaults to checking for a possible name from global docassemble config 9 | al_app_name = get_config('appname', get_config('external hostname','CourtFormsOnline.org')) 10 | --- 11 | code: | 12 | # The name of your organization. Defaults to a similar value as `al_app_name`, 13 | # but is still distinct 14 | AL_ORGANIZATION_TITLE = get_config('appname', 'CourtFormsOnline') 15 | --- 16 | code: | 17 | AL_ORGANIZATION_HOMEPAGE = get_config('app homepage', 'https://courtformsonline.org') 18 | --- 19 | ########################### 20 | # localization and internationalization / i18n settings 21 | --- 22 | code: | 23 | AL_DEFAULT_COUNTRY = "US" 24 | --- 25 | code: | 26 | AL_DEFAULT_STATE = None 27 | --- 28 | code: | 29 | AL_DEFAULT_LANGUAGE = "en" 30 | --- 31 | ############################### 32 | # Features 33 | --- 34 | features: 35 | navigation: True 36 | cache documents: False 37 | javascript: 38 | - limit_upload_size.js 39 | --- 40 | id: set basic options 41 | mandatory: True 42 | code: | 43 | # Set some variables that we need in all the interviews 44 | multi_user = True 45 | allow_cron = True 46 | speak_text = get_config('voicerss', {}).get('enable', False) 47 | set_live_help_status(availability='available', mode='help',partner_roles=['housing','family law']) 48 | --- 49 | code: | 50 | form_approved_for_email_filing = False # Default is that the form is not e-filable 51 | --- 52 | code: | 53 | # al_form_type can be one of: 54 | # 1. Court-case types: 'starts_case','existing_case','appeal' 55 | # 3. Letter: "letter" 56 | # 4. Other: "other", "other_form" 57 | al_form_type = "other" # We won't assume this is a court case 58 | --- 59 | code: | 60 | if len(other_parties): 61 | set_parts(subtitle=str(users) + ' op. ' + str(other_parties)) 62 | else: 63 | set_parts(subtitle=str(users)) 64 | update_answer_title = True 65 | --- 66 | comment: | 67 | This placeholder is used in the intro screen. 68 | 69 | In general, the title should follow the format 70 | "Ask the court for..." or "Respond to a ..." with a short, plain language 71 | description of what using the interview will accomplish. 72 | code: | 73 | interview_short_title = "[PLACEHOLDER: Ask the court for a ...]" 74 | --- 75 | code: | 76 | metadata_title = next(iter(interview_metadata.elements.values()),{}).get('title') 77 | 78 | # Check if the title was an actual text entry, or None 79 | if metadata_title: 80 | interview_short_title = "Ask the court for a " + metadata_title 81 | else: 82 | # This branch will happen for Docx where the interview_metadata dictionary 83 | # was never defined. 84 | interview_short_title = "[PLACEHOLDER: Ask the court for a ...]" 85 | --- 86 | imports: 87 | - importlib 88 | - docassemble.AssemblyLine # INSTALLED version of the package 89 | --- 90 | code: | 91 | try: 92 | al_version = "AL-" + str(docassemble.AssemblyLine.__version__) 93 | except: 94 | al_version = "" 95 | --- 96 | code: | 97 | try: 98 | package_version_number = str(importlib.import_module(current_context().package).__version__) 99 | except: 100 | package_version_number = "playground" 101 | --- 102 | code: | 103 | AL_DEFAULT_OVERFLOW_MESSAGE = "..." 104 | --- 105 | code: | 106 | al_form_requires_digital_signature = True 107 | --- 108 | code: | 109 | al_person_answering = "user" 110 | --- 111 | code: | 112 | al_typed_signature_prefix = "/s/" 113 | --- 114 | code: | 115 | # Can be an exact path or just a name, in which case we will search /usr/share/fonts and /var/www/.fonts for a matching file ending in .ttf 116 | al_typed_signature_font = "/usr/share/fonts/truetype/google-fonts/BadScript-Regular.ttf" -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/al_terms_of_use.yml: -------------------------------------------------------------------------------- 1 | --- 2 | template: al_terms_of_use 3 | subject: "" 4 | content: | 5 |

Terms of Use and Privacy Policy

6 | 7 | This website is free to use. 8 | % if get_config("default title") and get_config("default title") != "docassemble": 9 | ${ get_config("default title") } 10 | % else: 11 | ${ AL_ORGANIZATION_TITLE } 12 | % endif 13 | hosts the site. 14 | 15 | % if (not get_config("default title") == AL_ORGANIZATION_TITLE) and get_config("default title") != "docassemble": 16 | And it includes content from ${ AL_ORGANIZATION_TITLE }. 17 | % endif 18 | 19 |

You need to know

20 | 21 | * **This website does not provide legal advice**. 22 | % if AL_DEFAULT_COUNTRY == "US": 23 | If you need legal advice, you can use [LSC.gov](https://www.lsc.gov/about-lsc/what-legal-aid/i-need-legal-help) to find a lawyer. 24 | % else: 25 | If you need legal adice, call your local legal aid organization or lawyer referral service. 26 | % endif 27 | * The information and documents on this website have no warranty. We provide the information “as-is.” By using the site, you agree not to hold 28 | % if get_config("default title") and get_config("default title") != "docassemble": 29 | ${ get_config("default title") } 30 | % else: 31 | ${ AL_ORGANIZATION_TITLE } 32 | % endif 33 | or the information providers on this site liable. 34 | * We work hard to keep the information on the site up to date. Lawyers drafted and reviewed the documents this site uses. But laws and local rules change over time. These changes can make a document unenforceable when you use it. 35 | * We do our best to keep the site working! To do that, we allow you to submit feedback so we can track problems on the site. But we cannot provide individual technical support. 36 | 37 |

To use this site

38 | 39 | * You need to be at least 13. 40 | * This site is for **anyone** 13 and over. For example: you can be the person who needs the help, a lawyer, or a social worker. 41 | * You may not sell the information or the contents of this site. But you can charge for your own time. 42 | * Please keep your use fair. Do not use this site in a way that is illegal or that makes it harder for other people to use. 43 | 44 |

Information we and others collect

45 | 46 | * We collect the information that you type to help you complete your forms. We delete all information ${ get_config("interview delete days", 90)} days after you last update it. You can also delete information immediately. 47 | * We log information including IP addresses and web browsers from all visitors. We use this information to keep our site secure. We keep logs for up to 180 days. 48 | * We use Google Maps to help fill in addresses automatically. This feature sends your IP address to Google Maps to get your approximate location. 49 | * We use Google Analytics to learn how people use our website. This helps us understand which pages are hardest to use. Google may use this information to show you better advertisements. 50 | * We use email and text message delivery services that may keep their own records of any messages you send. If you choose to log in with your phone number, this may include a record of the times you log in to the site. 51 | 52 |

We keep your information safe

53 | 54 | * We use the same technology that banks and online stores use to keep your information safe. We use: 55 | * 256 bit encryption 56 | * Software firewalls 57 | * Limited access controls 58 | * Continual monitoring 59 | * Regular security updates 60 | 61 |

Sharing your information

62 | 63 | * We never sell your information. 64 | * We share anonymous information with a small number of nonprofits and researchers. We use the information to improve our site and to improve the delivery of free legal help. 65 | * When you visit some pages of the site, the site may ask if you want to apply to get a free lawyer from legal aid. You get to decide if you want to share your personal information. 66 | * You may choose to email completed forms or share a link to your in-progress work at any time to any person. 67 | 68 |

These Terms of Use can change

69 | 70 | We may change these Terms of Use at any time with no notice. Please review the terms on a regular basis to see any changes. 71 | 72 | If you use the site after we make changes, you accept our changed Terms of Use. 73 | 74 |

The laws of 75 | % if state_name(AL_DEFAULT_STATE, country_code=AL_DEFAULT_COUNTRY): 76 | ${ state_name(AL_DEFAULT_STATE, country_code=AL_DEFAULT_COUNTRY) } 77 | % elif AL_DEFAULT_COUNTRY == "US": 78 | the United States 79 | % else: 80 | ${ country_name(AL_DEFAULT_COUNTRY) } 81 | % endif 82 | apply to these Terms of Use

83 | 84 | If a judge decides that one of the terms in this Terms of Use is unenforceable, it will not change the terms that remain. 85 | 86 |

If you still have questions

87 | 88 | Send any questions to: 89 | 90 | ${ get_config("server administrator contact address", get_config("server administrator email", AL_ORGANIZATION_TITLE)) } -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/assembly_line.yml: -------------------------------------------------------------------------------- 1 | # For usage, see https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/framework/magic_variables 2 | --- 3 | include: 4 | - al_language.yml 5 | - al_settings.yml 6 | - al_code.yml 7 | - al_saved_sessions.yml 8 | - al_visual.yml 9 | - al_document.yml 10 | - ql_baseline.yml 11 | - docassemble.ALToolbox:phone-number-validation.yml 12 | - docassemble.ALToolbox:display_template.yml 13 | --- 14 | modules: 15 | - docassemble.ALToolbox.misc 16 | - docassemble.ALToolbox.copy_button 17 | - docassemble.ALToolbox.save_input_data 18 | - .al_general 19 | - .al_document 20 | - .al_courts 21 | - docassemble.GithubFeedbackForm.github_issue 22 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/assembly_line_unstyled.yml: -------------------------------------------------------------------------------- 1 | comment: | 2 | The purpose of this YAML file is to allow you to make use of all of the Assembly 3 | Line standard questions, etc. with one include, but eliminate all code that 4 | changes the visual appearance of your interview. 5 | --- 6 | include: 7 | - ql_baseline.yml 8 | - al_settings.yml 9 | - al_code.yml 10 | - al_saved_sessions.yml 11 | - al_document.yml 12 | --- 13 | modules: 14 | - docassemble.ALToolbox.misc 15 | - docassemble.ALToolbox.copy_button 16 | - .al_general 17 | - .al_document 18 | - .al_courts 19 | - docassemble.GithubFeedbackForm.github_issue 20 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/demo_search_sessions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - assembly_line.yml 4 | --- 5 | metadata: 6 | title: | 7 | Search sessions demo 8 | required privileges: 9 | - admin 10 | - developer 11 | - advocate 12 | --- 13 | mandatory: True 14 | code: | 15 | search_keyword 16 | if not matching_results: 17 | no_matches 18 | show_results 19 | show_selected_interview 20 | --- 21 | id: search_keyword 22 | question: | 23 | What would you like to search for? 24 | subquestion: | 25 | Searching is not case sensitive. 26 | fields: 27 | - Keyword: search_keyword 28 | - Limit metadata column: limit_column_name 29 | required: False 30 | - To value: limit_column_value 31 | required: False 32 | --- 33 | code: | 34 | if limit_column_name: 35 | matching_results = find_matching_sessions( 36 | search_keyword, user_id="all", metadata_filters = { 37 | limit_column_name: (limit_column_value, "ILIKE") 38 | } 39 | ) 40 | else: 41 | matching_results = find_matching_sessions(search_keyword, user_id="all") 42 | --- 43 | event: no_matches 44 | question: | 45 | We didn't find any matches 46 | subquestion: | 47 | Check your spelling and try again. 48 | --- 49 | continue button field: show_results 50 | question: | 51 | We found ${ len(matching_results) } results that match your keyword 52 | subquestion: | 53 | % for result in matching_results: 54 | * ${ nice_interview_subtitle(result, exclude_identical=False)} 55 | ${ result["modtime"]} 56 | % endfor 57 | 58 | Raw results: 59 |
60 | ${ matching_results } 61 |
62 | fields: 63 | - Which session do you want to load: interview_to_load 64 | datatype: integer 65 | code: | 66 | [{idx: f"{nice_interview_subtitle(answer, exclude_identical=False)}" + f' ({answer.get("modtime", DAEmpty()).strftime("%B %d, %Y")})'} for idx, answer in enumerate(matching_results)] 67 | --- 68 | event: show_selected_interview 69 | id: show the selected interview 70 | question: | 71 | Here is some information from the session you chose 72 | subquestion: | 73 | Filename: ${ matching_results[interview_to_load]["filename"] } [BR] 74 | Session ID: ${ matching_results[interview_to_load]["key"] } 75 | 76 |
77 |
${ get_filtered_session_variables_string(filename=matching_results[interview_to_load]["filename"], session_id=matching_results[interview_to_load]["key"]) }
78 |
Developer warning**: missing question for `${ x.instanceName }` 97 | content: | 98 | **This warning only appears in developer mode** 99 | 100 | It looks like you are missing a question to define a variable `${ x.instanceName }`. 101 | 102 | Try adding a question block like: 103 | 104 | % if x.data_type_guess() in ["int", "bool"]: 105 |
106 |   ---
107 |   id: amount of ${ x.instanceName }
108 |   question: |
109 |     How much is ${ x.label if hasattr(x, "label") else x.object_name() }?
110 |   fields:
111 |     - ${ x.label if hasattr(x, "label") else x.object_name() }: ${ x.instanceName }
112 |       datatype: currency
113 |   
114 | % else: 115 |
116 |   ---
117 |   id: ${ x.instanceName }
118 |   question: |
119 |     What is ${ x.label if hasattr(x, "label") else x.object_name() }?
120 |   fields:
121 |     - ${ x.label if hasattr(x, "label") else x.object_name() }: ${ x.instanceName }
122 |   
123 | % endif 124 | 125 | If you do not do anything, your end user may see this question instead 126 | of one you have customized. 127 | 128 | This warning message will not appear when this interview is installed on a production 129 | server. 130 | --- 131 | if: | 132 | hasattr(x, "_catchall_options") 133 | generic object: DACatchAll 134 | question: | 135 | ${ x.label if hasattr(x, "label") else x.object_name() }? 136 | subquestion: | 137 | % if get_config("debug") and al_warn_catchall_questions: 138 | ${ collapse_template(x.catchall_question_explanation) } 139 | % endif 140 | fields: 141 | - ${ x.label if hasattr(x, "label") else x.object_name() }: x.value 142 | code: | 143 | x._catchall_options 144 | validation code: | 145 | define(x.instanceName, x.value) 146 | --- 147 | template: dacatchall_css_template 148 | content: | 149 | % if get_config("debug") and al_warn_catchall_questions: 150 | 157 | % endif 158 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/ql_namechange.yml: -------------------------------------------------------------------------------- 1 | --- 2 | id: consent of parent 1 3 | question: | 4 | Do you consent to ${ children[0].preferred_name }'s name change? 5 | fields: 6 | - I consent: users[0].consented_to_name_change 7 | datatype: yesnoradio 8 | --- 9 | id: consent of parent 1 10 | question: | 11 | Does ${ users[1] } consent to ${ children[0].preferred_name }'s name change? 12 | fields: 13 | - ${ users[1] } consents: users[0].consented_to_name_change 14 | datatype: yesnoradio 15 | --- 16 | id: consent of parent 1 attached 17 | question: | 18 | Is your consent attached? 19 | fields: 20 | - My consent is attached: users[0].parent_consent_attached 21 | datatype: yesnoradio 22 | - Why not?: users[0].no_consent_attached_explanation 23 | datatype: area 24 | rows: 2 25 | show if: 26 | variable: users[0].parent_consent_attached 27 | is: False 28 | --- 29 | id: consent of parent 2 attached 30 | question: | 31 | Is ${ users[1] }'s consent attached? 32 | fields: 33 | - ${ users[1] }'s consent is attached: users[0].parent_consent_attached 34 | datatype: yesnoradio 35 | - Why not?: users[1].no_consent_attached_explanation 36 | datatype: area 37 | rows: 2 38 | show if: 39 | variable: users[1].parent_consent_attached 40 | is: False -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/reserved_words.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mandatory: True 3 | code: | 4 | import docassemble.base.util 5 | import keyword 6 | import docassemble.AssemblyLine.al_general 7 | import docassemble.AssemblyLine.al_document 8 | import docassemble.AssemblyLine.language 9 | import docassemble.AssemblyLine.sessions 10 | import docassemble.ALToolbox.misc 11 | 12 | all_reserved = set(docassemble.base.util.__all__ + 13 | docassemble.AssemblyLine.al_general.__all__ + 14 | docassemble.AssemblyLine.al_document.__all__ + 15 | docassemble.AssemblyLine.language.__all__ + 16 | docassemble.AssemblyLine.sessions.__all__ + 17 | docassemble.ALToolbox.misc.__all__ + 18 | keyword.kwlist + 19 | list(dir(__builtins__)) + [ 20 | "_attachment_email_address", 21 | "_attachment_include_editable", 22 | "_back_one", 23 | "_checkboxes", 24 | "_datatypes", 25 | "_email_attachments", 26 | "_files", 27 | "_question_number", 28 | "_question_name", 29 | "_save_as", 30 | "_success", 31 | "_the_image", 32 | "_track_location", 33 | "_tracker", 34 | "_varnames", 35 | "_internal", 36 | "nav", 37 | "session_local", 38 | "device_local", 39 | "user_local", 40 | "url_args", 41 | "role_needed", 42 | "x", 43 | "i", 44 | "j", 45 | "k", 46 | "l", 47 | "m", 48 | "n", 49 | "role", 50 | "speak_text", 51 | "track_location", 52 | "multi_user", 53 | "menu_items", 54 | "allow_cron", 55 | "incoming_email", 56 | "role_event", 57 | "cron_hourly", 58 | "cron_daily", 59 | "cron_weekly", 60 | "cron_monthly", 61 | "_internal", 62 | "allow_cron", 63 | "cron_daily", 64 | "cron_hourly", 65 | "cron_monthly", 66 | "cron_weekly", 67 | "caller", 68 | "device_local", 69 | "loop", 70 | "incoming_email", 71 | "menu_items", 72 | "multi_user", 73 | "nav", 74 | "role_event", 75 | "role_needed", 76 | "row_index", 77 | "row_item", 78 | "self", 79 | "session_local", 80 | "speak_text", 81 | "STOP_RENDERING", 82 | "track_location", 83 | "url_args", 84 | "user_local", 85 | "user_dict", 86 | "allow_cron", 87 | ] 88 | ) 89 | --- 90 | mandatory: True 91 | code: | 92 | json_response(sorted(all_reserved)) -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_al_table_document.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - al_package.yml 4 | --- 5 | table: test_table.table 6 | rows: | 7 | [DAObject(attribute=m) for m in range(10)] 8 | columns: 9 | - Column 1: | 10 | row_item.attribute 11 | --- 12 | mandatory: True 13 | code: | 14 | intro 15 | download 16 | --- 17 | objects: 18 | - test_table: ALTableDocument.using(title="Table test", filename="test", enabled=True) 19 | --- 20 | objects: 21 | - al_court_bundle: ALDocumentBundle.using(elements=[test_table], filename='court_bundle.pdf', title='All forms to send to the court', enabled=False) 22 | --- 23 | field: intro 24 | question: | 25 | Intro 26 | --- 27 | imports: 28 | - mimetypes 29 | --- 30 | event: download 31 | question: | 32 | Download 33 | subquestion: | 34 | 35 | ${ al_court_bundle.download_list_html() } -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_aladdendum.yml: -------------------------------------------------------------------------------- 1 | --- 2 | comment: | 3 | File to informally test the addendum class interactively. Does not have full coverage. 4 | --- 5 | include: 6 | - assembly_line.yml 7 | --- 8 | mandatory: True 9 | code: | 10 | intro_screen 11 | ending_screen 12 | --- 13 | question: | 14 | Test addendum class 15 | continue button field: intro_screen 16 | --- 17 | objects: 18 | - custom_list_item: DAList.using(object_type=DAObject, complete_attribute=["location", "address.address"]) 19 | - custom_list_item[i].address: ALAddress 20 | - al_addendum_test_attachment: ALDocument.using(title="Test", filename="addendum_test", enabled=True, has_addendum=True, default_overflow_message=" [...]") 21 | --- 22 | objects: 23 | - al_user_bundle: ALDocumentBundle.using(elements=[al_addendum_test_attachment], title="addendum_test", filename="addendum_test", enabled=True) 24 | --- 25 | code: | 26 | al_addendum_test_attachment.overflow_fields["custom_list_item"].overflow_trigger = 3 27 | al_addendum_test_attachment.overflow_fields["custom_list_item"].label = "Custom list" 28 | al_addendum_test_attachment.overflow_fields.gathered = True 29 | --- 30 | question: | 31 | Are there any custom list items? 32 | fields: 33 | - no label: custom_list_item.there_are_any 34 | datatype: yesnoradio 35 | --- 36 | question: | 37 | Are there any more? 38 | subquestion: | 39 | To add trigger addendum, add at least 4. 40 | fields: 41 | - no label: custom_list_item.there_is_another 42 | datatype: yesnoradio 43 | --- 44 | question: | 45 | Tell me about the ${ ordinal(i) } list item 46 | fields: 47 | - Location: custom_list_item[i].location 48 | - Address: custom_list_item[i].address.address 49 | address autocomplete: True 50 | - City: custom_list_item[i].address.city 51 | - State: custom_list_item[i].address.state 52 | - Zip: custom_list_item[i].address.zip 53 | validation code: | 54 | custom_list_item[i].address.geocode() 55 | --- 56 | attachment: 57 | variable name: al_addendum_test_attachment[i] 58 | content: | 59 | % for item in custom_list_item[:3]: 60 | # Location ${ item.location } 61 | 62 | ${ item.address.block() } 63 | 64 | % endfor 65 | --- 66 | event: ending_screen 67 | question: | 68 | Download 69 | subquestion: | 70 | ${ al_user_bundle.download_list_html() } -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_alcourts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - assembly_line.yml 4 | --- 5 | metadata: 6 | title: | 7 | Test courts classes 8 | --- 9 | # TODO: When the testing framework implements the back button, 10 | # consider reducing this to one page only and simply going back 11 | # and forth. 12 | mandatory: True 13 | code: | 14 | court_with_all_fields_name 15 | court_with_all_fields_result 16 | 17 | name_contains_city_name 18 | name_contains_city_result 19 | 20 | end_alcourt_tests 21 | --- 22 | # User fills in all fields 23 | --- 24 | objects: 25 | - court_with_all_fields_address: Address 26 | --- 27 | depends on: 28 | - court_with_all_fields_name 29 | - court_with_all_fields_address 30 | need: 31 | - court_with_all_fields_name 32 | - court_with_all_fields_address 33 | code: | 34 | court_with_all_fields = ALCourt('court_with_all_fields') 35 | court_with_all_fields.name = court_with_all_fields_name 36 | court_with_all_fields.address = court_with_all_fields_address 37 | --- 38 | code: | 39 | court_with_all_fields.description = 'Description of the court that has all its fields filled in defined by the developer.' 40 | --- 41 | id: court with all fields 42 | question: | 43 | User fills all fields 44 | fields: 45 | - Name: court_with_all_fields_name 46 | - Address: court_with_all_fields_address.address 47 | address autocomplete: True 48 | required: False 49 | - Suite: court_with_all_fields_address.unit 50 | required: False 51 | - City: court_with_all_fields_address.city 52 | required: False 53 | - State: court_with_all_fields_address.state 54 | required: False 55 | - Postal code: court_with_all_fields_address.zip 56 | required: False 57 | --- 58 | # TODO: test `.from_row()`, `.geolocate()` 59 | id: court_with_all_fields_result 60 | question: | 61 | Court info when all fields filled 62 | subquestion: | 63 | `court_with_all_fields.short_label()`: 64 | 65 | ${ court_with_all_fields.short_label() } 66 | 67 | `court_with_all_fields.short_label_and_address()`: 68 | 69 | ${ court_with_all_fields.short_label_and_address() } 70 | 71 | `court_with_all_fields.short_description()`: 72 | 73 | ${ court_with_all_fields.short_description() } 74 | continue button field: court_with_all_fields_result 75 | --- 76 | # Court name includes city 77 | --- 78 | objects: 79 | - name_contains_city_address: Address 80 | --- 81 | depends on: 82 | - name_contains_city_name 83 | - name_contains_city_address 84 | need: 85 | - name_contains_city_name 86 | - name_contains_city_address 87 | code: | 88 | name_contains_city = ALCourt('name_contains_city') 89 | name_contains_city.name = name_contains_city_name 90 | name_contains_city.address = name_contains_city_address 91 | --- 92 | code: | 93 | name_contains_city.description = 'Description of the court that has just its name filled in defined by the developer.' 94 | --- 95 | id: name has city 96 | question: | 97 | Name contains city name 98 | fields: 99 | - Name: name_contains_city_name 100 | - Address: name_contains_city_address.address 101 | address autocomplete: True 102 | required: False 103 | - Suite: name_contains_city_address.unit 104 | required: False 105 | - City: name_contains_city_address.city 106 | required: False 107 | - State: name_contains_city_address.state 108 | required: False 109 | - Postal code: name_contains_city_address.zip 110 | required: False 111 | --- 112 | # TODO: test `.from_row()`, `.geolocate()` 113 | id: name_contains_city_result 114 | question: | 115 | Court info when all fields filled 116 | subquestion: | 117 | `name_contains_city.short_label()`: 118 | 119 | ${ name_contains_city.short_label() } 120 | 121 | `name_contains_city.short_label_and_address()`: 122 | 123 | ${ name_contains_city.short_label_and_address() } 124 | 125 | `name_contains_city.short_description()`: 126 | 127 | ${ name_contains_city.short_description() } 128 | continue button field: name_contains_city_result 129 | --- 130 | id: end alcourt tests 131 | event: end_alcourt_tests 132 | question: The end of ALCourt tests 133 | subquestion: | 134 | Tests still needed: 135 | 136 | - ALCourt `.geolocate()` 137 | - ALCourt `.from_row()` 138 | - ALCourtLoader functionality 139 | --- -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_aldocument.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - al_package.yml 4 | --- 5 | metadata: 6 | title: | 7 | Test ALDocument class 8 | --- 9 | continue button field: intro_screen 10 | question: | 11 | Intro screen 12 | --- 13 | mandatory: True 14 | code: | 15 | intro_screen 16 | as_foo_defaults 17 | download_list_html_defaults 18 | #download_html_defaults 19 | email_defaults 20 | as_foo_custom 21 | download_list_html_custom_1 22 | download_list_html_custom_2 23 | download_list_html_custom_3 24 | #download_html_custom_1 25 | #download_html_custom_2 26 | email_custom 27 | end_aldocument_tests 28 | --- 29 | objects: 30 | # No addenda, all start enabled 31 | - multi_bundle_1: ALDocumentBundle.using(elements=[pdf_1, docx_1, notice_to_quit], title="Multiple docs 1", filename="multi_bundle_1" ) 32 | - single_pdf_bundle_1: ALDocumentBundle.using(elements=[pdf_1], title="One pdf", filename="single_pdf_1" ) 33 | - single_docx_bundle_1: ALDocumentBundle.using(elements=[docx_1], title="One docx", filename="single_docx_1" ) 34 | --- 35 | objects: 36 | - pdf_1: ALDocument.using( title="PDF 1 with a lot of text for the title of this document", filename="pdf_doc_1", enabled=True, has_addendum=False ) 37 | - docx_1: ALDocument.using( title="Docx 1 also with a lot of text for the title of this document", filename="docx_doc_1", enabled=True, has_addendum=False ) 38 | - notice_to_quit: ALDocumentUpload.using(title="Notice to Quit", filename="notice_to_quit") 39 | --- 40 | attachment: 41 | variable name: pdf_1[i] 42 | pdf template file: test_aldocument_pdf_1.pdf 43 | filename: pdf_1 44 | fields: 45 | - "sample_field": "Sample input" 46 | --- 47 | attachment: 48 | variable name: docx_1[i] 49 | docx template file: test_aldocument_docx_1.docx 50 | filename: docx_1 51 | --- 52 | # TODO: Do we/how do we test that correct defaults have been used? Esp. for args like `refresh` 53 | --- 54 | # Caching seems to be working - it took the same amount of time to show with just multi_bundle as with all of them. 55 | id: as_foo_defaults 56 | continue button field: as_foo_defaults 57 | question: | 58 | Get condensed bundle functions defaults 59 | subquestion: | 60 | 61 | multi_bundle_1.as_pdf() 62 | 63 | ${ multi_bundle_1.as_pdf() } 64 | 65 | multi_bundle_1.preview() 66 | 67 | ${ multi_bundle_1.preview() } 68 | 69 | multi_bundle_1.as_zip() 70 | 71 | ${ multi_bundle_1.as_zip() } 72 | 73 | --- 74 | 75 | single_pdf_bundle_1.as_pdf() 76 | 77 | ${ single_pdf_bundle_1.as_pdf() } 78 | 79 | single_pdf_bundle_1.preview() 80 | 81 | ${ single_pdf_bundle_1.preview() } 82 | 83 | single_pdf_bundle_1.as_zip() 84 | 85 | ${ single_pdf_bundle_1.as_zip() } 86 | 87 | --- 88 | 89 | single_docx_bundle_1.as_pdf() 90 | 91 | ${ single_docx_bundle_1.as_pdf() } 92 | 93 | single_docx_bundle_1.preview() 94 | 95 | ${ single_docx_bundle_1.preview() } 96 | 97 | single_docx_bundle_1.as_zip() 98 | 99 | ${ single_docx_bundle_1.as_zip() } 100 | 101 | --- 102 | --- 103 | id: download_list_html_defaults 104 | continue button field: download_list_html_defaults 105 | question: | 106 | download_list_html() defaults 107 | subquestion: | 108 | All should be pdfs. 109 | 110 | Note: We're deprecating `download_html()` and removing it, so it will not be tested in here. 111 | 112 | multi_bundle_1.download_list_html() with send button for alignment comparison 113 | 114 | ${ multi_bundle_1.download_list_html() } 115 | 116 | ${ multi_bundle_1.send_button_html() } 117 | 118 | multi_bundle_1.download_list_html( include_email=True ) 119 | 120 | ${ multi_bundle_1.download_list_html( include_email=True ) } 121 | 122 | single_pdf_bundle_1.download_list_html() 123 | 124 | ${ single_pdf_bundle_1.download_list_html() } 125 | 126 | single_pdf_bundle_1.download_list_html( include_email=True ) 127 | 128 | ${ single_pdf_bundle_1.download_list_html( include_email=True ) } 129 | 130 | single_docx_bundle_1.download_list_html() 131 | 132 | ${ single_docx_bundle_1.download_list_html() } 133 | --- 134 | # Keeping this in here until method is completely removed just in case other decisions are made 135 | #id: download_html_defaults 136 | #continue button field: download_html_defaults 137 | #question: | 138 | # download_html() defaults 139 | #subquestion: | 140 | # multi_bundle_1.download_html() 141 | # 142 | # ${ multi_bundle_1.download_html() } 143 | --- 144 | id: email_defaults 145 | continue button field: email_defaults 146 | question: | 147 | send_button_html() defaults 148 | subquestion: | 149 | multi_bundle_1.send_button_html() 150 | 151 | ${ multi_bundle_1.send_button_html() } 152 | 153 | single_pdf_bundle_1.send_button_html() 154 | 155 | ${ single_pdf_bundle_1.send_button_html() } 156 | 157 | single_docx_bundle_1.send_button_html() 158 | 159 | ${ single_docx_bundle_1.send_button_html() } 160 | --- 161 | id: as_foo_custom 162 | continue button field: as_foo_custom 163 | question: | 164 | Get condensed bundles with custom args 165 | subquestion: | 166 | - .as_pdf(), .preview(), .as_zip() 167 | - All possible custom args passed in. Not sure how to test use of these. We'll at least test that nothing errors. 168 | 169 | multi_bundle_1.as_pdf( key="test_key", refresh=False ) 170 | 171 | ${ multi_bundle_1.as_pdf( key="test_key", refresh=False ) } 172 | 173 | multi_bundle_1.preview( refresh=False ) 174 | 175 | ${ multi_bundle_1.preview( refresh=False ) } 176 | 177 | multi_bundle_1.as_zip( key="test_key", refresh=False, title="Custom title" ) 178 | 179 | ${ multi_bundle_1.as_zip( key="test_key", refresh=False, title="Custom title" ) } 180 | 181 | --- 182 | 183 | single_pdf_bundle_1.as_pdf( key="test_key", refresh=False ) 184 | 185 | ${ single_pdf_bundle_1.as_pdf( key="test_key", refresh=False ) } 186 | 187 | single_pdf_bundle_1.preview( refresh=False ) 188 | 189 | ${ single_pdf_bundle_1.preview( refresh=False ) } 190 | 191 | single_pdf_bundle_1.as_zip( key="test_key", refresh=False, title="Custom title" ) 192 | 193 | ${ single_pdf_bundle_1.as_zip( key="test_key", refresh=False, title="Custom title" ) } 194 | 195 | --- 196 | 197 | single_docx_bundle_1.as_pdf( key="test_key", refresh=False ) 198 | 199 | ${ single_docx_bundle_1.as_pdf( key="test_key", refresh=False ) } 200 | 201 | single_docx_bundle_1.preview( refresh=False ) 202 | 203 | ${ single_docx_bundle_1.preview( refresh=False ) } 204 | 205 | single_docx_bundle_1.as_zip( key="test_key", refresh=False, title="Custom title" ) 206 | 207 | ${ single_docx_bundle_1.as_zip( key="test_key", refresh=False, title="Custom title" ) } 208 | 209 | --- 210 | --- 211 | comment: | 212 | Defaults are: 213 | key:str='final', 214 | format:str='pdf', 215 | view:bool=True, 216 | refresh:bool=True, 217 | include_zip:bool=True, 218 | view_label:str="View", 219 | view_icon:str="eye", 220 | download_label:str="Download", 221 | download_icon:str="download", 222 | include_email:bool=False, 223 | send_label:str="Send", 224 | send_icon:str="envelope", 225 | zip_label:str="Download zip", 226 | zip_icon:str="file-archive" 227 | id: download_list_html_custom_1 228 | continue button field: download_list_html_custom_1 229 | question: | 230 | download_list_html() with custom args (1) 231 | subquestion: | 232 | Unchanged: 233 | 234 | - Keep "view" as True 235 | - Keep "include_zip" as True 236 | 237 | multi_bundle_1.download_list_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square", include_email=True, zip_label="Custom zip label", zip_icon="clock", zip_row_label="Custom zip row label", send_label="Custom send label", send_icon="search" ) 238 | 239 | ${ multi_bundle_1.download_list_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square", include_email=True, zip_label="Custom zip label", zip_icon="clock", zip_row_label="Custom zip row label", send_label="Custom send label", send_icon="search" ) } 240 | 241 | --- 242 | 243 | single_pdf_bundle_1.download_list_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square", include_email=True, zip_label="Custom zip label", zip_icon="clock", send_label="Custom send label", send_icon="search" ) 244 | 245 | ${ single_pdf_bundle_1.download_list_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square", include_email=True, zip_label="Custom zip label", zip_icon="clock", send_label="Custom send label", send_icon="search" ) } 246 | 247 | --- 248 | 249 | single_docx_bundle_1.download_list_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square", include_email=True, zip_label="Custom zip label", zip_icon="clock", send_label="Custom send label", send_icon="search" ) 250 | 251 | ${ single_docx_bundle_1.download_list_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square", include_email=True, zip_label="Custom zip label", zip_icon="clock", send_label="Custom send label", send_icon="search" ) } 252 | 253 | --- 254 | --- 255 | id: download_list_html_custom_2 256 | continue button field: download_list_html_custom_2 257 | question: | 258 | download_list_html() with custom args (2) 259 | subquestion: | 260 | multi_bundle_1.download_list_html( view=False, include_zip=False ) 261 | 262 | ${ multi_bundle_1.download_list_html( view=False, include_zip=False ) } 263 | 264 | --- 265 | single_pdf_bundle_1.download_list_html( view=False, include_zip=False ) 266 | 267 | ${ single_pdf_bundle_1.download_list_html( view=False, include_zip=False ) } 268 | 269 | --- 270 | single_docx_bundle_1.download_list_html( view=False, include_zip=False ) 271 | 272 | ${ single_docx_bundle_1.download_list_html( view=False, include_zip=False ) } 273 | 274 | --- 275 | --- 276 | id: download_list_html_custom_3 277 | continue button field: download_list_html_custom_3 278 | question: | 279 | download_list_html() with custom args (3) 280 | subquestion: | 281 | 282 | multi_bundle_1.download_list_html( view=True, format="docx" ) 283 | 284 | ${ multi_bundle_1.download_list_html( view=True, format="docx" ) } 285 | 286 | _Zip should contain 3 files: docx, pdf, and PDF conversion of docx._ 287 | 288 | --- 289 | 290 | multi_bundle_1.download_list_html( view=False, format="docx" ) 291 | 292 | ${ multi_bundle_1.download_list_html( view=False, format="docx" ) } 293 | 294 | _Zip should contain 2 files: docx, pdf._ 295 | --- 296 | # Keeping these in here until method is completely removed just in case other decisions are made 297 | #comment: | 298 | # Defaults: 299 | # key:str ='final', 300 | # format:str ='pdf', 301 | # view:bool=True, 302 | # refresh:bool=True, 303 | # view_label:str='View', 304 | # view_icon:str='eye', 305 | # download_label:str="Download", 306 | # download_icon:str="download" 307 | #id: download_html_custom_1 308 | #continue button field: download_html_custom_1 309 | #question: | 310 | # download_html() with custom args (1) 311 | #subquestion: | 312 | # WARNING: 313 | # format="docx" not yet functional 314 | # 315 | # Unchanged: 316 | # 317 | # - Keep "view" as True 318 | # 319 | # multi_bundle_1.download_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square" ) 320 | # 321 | # ${ multi_bundle_1.download_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square" ) } 322 | #--- 323 | #id: download_html_custom_2 324 | #continue button field: download_html_custom_2 325 | #question: | 326 | # download_html() with custom args (2) 327 | #subquestion: | 328 | # multi_bundle_1.download_html( view=False ) 329 | # 330 | # ${ multi_bundle_1.download_html( view=False ) } 331 | --- 332 | id: email_custom 333 | continue button field: email_custom 334 | question: | 335 | send_button_html() and send_button_to_html() with custom args 336 | subquestion: | 337 | ##### send_button_html() 338 | 339 | Send button which allows the user to edit the email address. 340 | 341 | multi_bundle_1.send_button_html( key="test_key", show_editable_checkbox=False, label="Custom send label", icon="search" ) 342 | 343 | ${ multi_bundle_1.send_button_html( key="test_key", show_editable_checkbox=False, label="Custom send label", icon="search" ) } 344 | 345 | --- 346 | single_pdf_bundle_1.send_button_html( key="test_key", show_editable_checkbox=False, label="Custom send label", icon="search" ) 347 | 348 | ${ single_pdf_bundle_1.send_button_html( key="test_key", show_editable_checkbox=False, label="Custom send label", icon="search" ) } 349 | 350 | --- 351 | single_docx_bundle_1.send_button_html( key="test_key", show_editable_checkbox=False, label="Custom send label", icon="search" ) 352 | 353 | ${ single_docx_bundle_1.send_button_html( key="test_key", show_editable_checkbox=False, label="Custom send label", icon="search" ) } 354 | 355 | --- 356 | 357 | ##### send_button_to_html() 358 | 359 | Send button that hides the recipient's email address. 360 | 361 | multi_bundle_1.send_button_to_html( "happy_feet@example.com", key="test_key", label="Custom send label", icon="search" ) 362 | 363 | ${ multi_bundle_1.send_button_to_html( "happy_feet@example.com", key="test_key", label="Custom send label", icon="search" ) } 364 | 365 | --- 366 | single_pdf_bundle_1.send_button_to_html( "happy_feet@example.com", key="test_key", label="Custom send label", icon="search" ) 367 | 368 | ${ single_pdf_bundle_1.send_button_to_html( "happy_feet@example.com", key="test_key", label="Custom send label", icon="search" ) } 369 | 370 | --- 371 | single_docx_bundle_1.send_button_to_html( "happy_feet@example.com", key="test_key", label="Custom send label", icon="search" ) 372 | 373 | ${ single_docx_bundle_1.send_button_to_html( "happy_feet@example.com", key="test_key", label="Custom send label", icon="search" ) } 374 | 375 | --- 376 | --- 377 | id: end aldocument tests 378 | event: end_aldocument_tests 379 | question: The end of ALDocument tests 380 | --- -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_aldocument_background_assembly.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - assembly_line.yml 4 | --- 5 | metadata: 6 | title: ALDocument Background Processing 7 | --- 8 | mandatory: True 9 | code: | 10 | intro_screen 11 | if not al_user_bundle.generate_preview_task.ready(): 12 | al_preview_waiting_screen 13 | preview_screen 14 | # if not al_user_bundle.generate_downloads_task.ready(): # Without DOCX 15 | if not al_user_bundle.generate_downloads_with_docx_task.ready(): # With DOCX 16 | al_download_waiting_screen 17 | download_screen 18 | --- 19 | continue button field: intro_screen 20 | question: | 21 | Intro screen 22 | --- 23 | id: preview_screen 24 | continue button field: preview_screen 25 | question: | 26 | Preview the documents 27 | subquestion: | 28 | Your documents are ready for preview. 29 | 30 | Click the thumbnail below to view the preview of your documents. It will open in a new tab. 31 | 32 | ${ al_user_bundle._preview_file } 33 | --- 34 | id: download 35 | event: download_screen 36 | question: | 37 | Download the documents 38 | subquestion: | 39 | Your documents are ready for download. 40 | 41 | ${ al_user_bundle.download_list_html(use_previously_cached_files=True, include_full_pdf=True) } 42 | --- 43 | objects: 44 | - al_user_bundle: ALDocumentBundle.using( 45 | elements = [pdf_1, docx_1], 46 | title = "Background processed bundle", 47 | enabled = True, 48 | filename = "background_processed_bundle", 49 | ) 50 | --- 51 | objects: 52 | - pdf_1: ALDocument.using( title="PDF 1 with a lot of text for the title of this document", filename="pdf_doc_1", enabled=True, has_addendum=False ) 53 | - docx_1: ALDocument.using( title="Docx 1 also with a lot of text for the title of this document", filename="docx_doc_1", enabled=True, has_addendum=False ) 54 | --- 55 | attachment: 56 | variable name: pdf_1[i] 57 | pdf template file: test_aldocument_pdf_1.pdf 58 | filename: pdf_1 59 | fields: 60 | - "sample_field": "Sample input" 61 | --- 62 | attachment: 63 | variable name: docx_1[i] 64 | docx template file: test_aldocument_docx_1.docx 65 | filename: docx_1 66 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_alexhibit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - al_package.yml 4 | --- 5 | mandatory: True 6 | code: | 7 | thumbnails_defaults 8 | download_list_defaults 9 | thumbnails_custom 10 | download_list_html_custom_1 11 | download_list_html_custom_2 12 | end_alexhibit_tests 13 | --- 14 | objects: 15 | - exhibits_bundle_defaults_1: ALDocumentBundle.using(elements=[exhibit_doc_defaults_1], filename="exhibits_bundle_defaults", title="Exhibits with defaults") 16 | # Commenting out until the custom exhibit features are implemented 17 | - exhibits_bundle_custom_1: ALDocumentBundle.using(elements=[exhibit_doc_custom_1], filename="exhibits_bundle_custom", title="Exhibits with custom args") 18 | --- 19 | template: custom_exhibit_args 20 | content: | 21 | Testing custom values for ALExhibitDocument args. Currently includes: 22 | 23 | - `include_table_of_contents=False` 24 | - `include_exhibit_cover_pages=False` 25 | --- 26 | template: no_custom_exhibit_args 27 | content: | 28 | Custom ALExhibitDocument args: 29 | 30 | - None 31 | --- 32 | objects: 33 | - exhibit_doc_defaults_1: ALExhibitDocument.using(title="Exhibits doc defaults", filename="exhibits_doc_defaults" ) 34 | # Changing these defaults: 35 | # `include_table_of_contents` default True 36 | # `include_exhibit_cover_pages` default True 37 | - exhibit_doc_custom_1: ALExhibitDocument.using(title="Exhibits doc custom args", filename="exhibits_doc_custom", include_table_of_contents=False, include_exhibit_cover_pages=False ) 38 | --- 39 | # Note on test screens: Screens test that custom ALDocumentBundle arguments and such are happy. As far as custom exhibit arguments, they're tested in instantiation of the custom exhibit document itself. Currently, each screen should test both the default exhibit document and the custom exhibit document (once custom arguments are implemented). 40 | --- 41 | id: thumbnails_defaults 42 | continue button field: thumbnails_defaults 43 | question: Default ALDocumentBundle thumbnail method args 44 | subquestion: | 45 | ${ no_custom_exhibit_args } 46 | 47 | exhibits_bundle_defaults_1.as_pdf() 48 | 49 | ${ exhibits_bundle_defaults_1.as_pdf() } 50 | 51 | exhibits_bundle_defaults_1.preview() 52 | 53 | ${ exhibits_bundle_defaults_1.preview() } 54 | 55 | exhibits_bundle_defaults_1.as_zip() 56 | 57 | ${ exhibits_bundle_defaults_1.as_zip() } 58 | 59 | --- 60 | 61 | ${ custom_exhibit_args } 62 | 63 | exhibits_bundle_custom_1.as_pdf() 64 | 65 | ${ exhibits_bundle_custom_1.as_pdf() } 66 | 67 | exhibits_bundle_custom_1.preview() 68 | 69 | ${ exhibits_bundle_custom_1.preview() } 70 | 71 | exhibits_bundle_custom_1.as_zip() 72 | 73 | ${ exhibits_bundle_custom_1.as_zip() } 74 | --- 75 | id: download_list_defaults 76 | continue button field: download_list_defaults 77 | question: Default ALDocumentBundle .download_list_html() args 78 | subquestion: | 79 | ${ no_custom_exhibit_args } 80 | 81 | ${ exhibits_bundle_defaults_1.download_list_html() } 82 | 83 | --- 84 | 85 | ${ custom_exhibit_args } 86 | 87 | ${ exhibits_bundle_custom_1.download_list_html() } 88 | --- 89 | id: thumbnails_custom 90 | continue button field: thumbnails_custom 91 | question: | 92 | Custom ALDocumentBundle args 93 | subquestion: | 94 | Custom ALDocumentBundle args: 95 | 96 | - `key="test_key"` 97 | - `refresh=False` 98 | - `title="Custom title"` 99 | 100 | --- 101 | 102 | ${ no_custom_exhibit_args } 103 | 104 | exhibits_bundle_defaults_1.as_pdf( key="test_key", refresh=False ) 105 | 106 | ${ exhibits_bundle_defaults_1.as_pdf( key="test_key", refresh=False ) } 107 | 108 | exhibits_bundle_defaults_1.preview( refresh=False ) 109 | 110 | ${ exhibits_bundle_defaults_1.preview( refresh=False ) } 111 | 112 | exhibits_bundle_defaults_1.as_zip( key="test_key", refresh=False, title="Custom title" ) 113 | 114 | ${ exhibits_bundle_defaults_1.as_zip( key="test_key", refresh=False, title="Custom title" ) } 115 | 116 | --- 117 | 118 | ${ custom_exhibit_args } 119 | 120 | exhibits_bundle_custom_1.as_pdf( key="test_key", refresh=False ) 121 | 122 | ${ exhibits_bundle_custom_1.as_pdf( key="test_key", refresh=False ) } 123 | 124 | exhibits_bundle_custom_1.preview( refresh=False ) 125 | 126 | ${ exhibits_bundle_custom_1.preview( refresh=False ) } 127 | 128 | exhibits_bundle_custom_1.as_zip( key="test_key", refresh=False, title="Custom title" ) 129 | 130 | ${ exhibits_bundle_custom_1.as_zip( key="test_key", refresh=False, title="Custom title" ) } 131 | --- 132 | id: download_list_html_custom_1 133 | continue button field: download_list_html_custom_1 134 | question: | 135 | ALDocumentBundle download_list_html() with custom args (1) 136 | subquestion: | 137 | Custom ALDocumentBundle args: 138 | 139 | - `key="test_key"` 140 | - `format="docx"` 141 | - `refresh=False` 142 | - `view_label="Custom view label"` 143 | - `view_icon="circle"` 144 | - `download_label="Custom download label"` 145 | - `download_icon="square"` 146 | - `zip_label="Custom zip label"` 147 | - `zip_icon="clock"` 148 | 149 | --- 150 | 151 | ${ no_custom_exhibit_args } 152 | 153 | exhibits_bundle_defaults_1.download_list_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square", zip_label="Custom zip label", zip_icon="clock" ) 154 | 155 | ${ exhibits_bundle_defaults_1.download_list_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square", zip_label="Custom zip label", zip_icon="clock" ) } 156 | 157 | --- 158 | 159 | ${ custom_exhibit_args } 160 | 161 | exhibits_bundle_custom_1.download_list_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square", zip_label="Custom zip label", zip_icon="clock" ) 162 | 163 | ${ exhibits_bundle_custom_1.download_list_html( key="test_key", format="docx", refresh=False, view_label="Custom view label", view_icon="circle", download_label="Custom download label", download_icon="square", zip_label="Custom zip label", zip_icon="clock" ) } 164 | --- 165 | id: download_list_html_custom_2 166 | continue button field: download_list_html_custom_2 167 | question: | 168 | ALDocumentBundle download_list_html() with custom args (2) 169 | subquestion: | 170 | Custom ALDocumentBundle args: 171 | 172 | - `view=False` 173 | - `include_zip=False` 174 | 175 | --- 176 | 177 | ${ no_custom_exhibit_args } 178 | 179 | exhibits_bundle_defaults_1.download_list_html( view=False, include_zip=False ) 180 | 181 | ${ exhibits_bundle_defaults_1.download_list_html( view=False, include_zip=False ) } 182 | 183 | --- 184 | 185 | ${ custom_exhibit_args } 186 | 187 | exhibits_bundle_custom_1.download_list_html( view=False, include_zip=False ) 188 | 189 | ${ exhibits_bundle_custom_1.download_list_html( view=False, include_zip=False ) } 190 | --- 191 | id: end alexhibit tests 192 | event: end_alexhibit_tests 193 | question: The end of ALExhibitDocument tests 194 | buttons: 195 | - Restart: restart 196 | --- 197 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_alexhibit_maxsize.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - al_package.yml 4 | --- 5 | mandatory: True 6 | code: | 7 | gather_main 8 | --- 9 | objects: 10 | - exhibits_bundle_defaults_1: ALDocumentBundle.using(elements=[exhibit_doc_defaults_1], filename="exhibits_bundle_defaults", title="Exhibits with defaults") 11 | --- 12 | objects: 13 | - exhibit_doc_defaults_1: ALExhibitDocument.using(title="Exhibits doc defaults", filename="exhibits_doc_defaults", maximum_size=1*1024*1024 ) 14 | --- 15 | id: gather_main 16 | event: gather_main 17 | question: Default ALDocumentBundle thumbnail method args 18 | subquestion: | 19 | exhibits_bundle_defaults_1.as_pdf() 20 | 21 | ${ exhibits_bundle_defaults_1.as_pdf() } 22 | 23 | ${ exhibits_bundle_defaults_1.as_pdf().size_in_bytes() } -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_bundle_parity.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - assembly_line.yml 4 | --- 5 | objects: 6 | - the_doc: ALDocument.using( 7 | title="The document", 8 | filename="the_document", 9 | enabled=True, 10 | has_addendum=False 11 | ) 12 | --- 13 | objects: 14 | - even_bundle: ALDocumentBundle.using( 15 | title="Even bundle", 16 | filename="the_bundle", 17 | elements=[ 18 | the_doc 19 | ], 20 | default_parity="even", 21 | enabled=True 22 | ) 23 | - odd_bundle: ALDocumentBundle.using( 24 | title="Even bundle", 25 | filename="the_bundle", 26 | elements=[ 27 | the_doc, 28 | the_doc 29 | ], 30 | enabled=True 31 | ) 32 | - bundle_with_default: ALDocumentBundle.using( 33 | title="Even bundle", 34 | filename="the_bundle", 35 | elements=[ 36 | the_doc 37 | ], 38 | default_parity="even", 39 | enabled=True 40 | ) 41 | 42 | --- 43 | objects: 44 | - bundle_of_bundles: ALDocumentBundle.using( 45 | title="Bundle of bundles", 46 | filename="the_bundle", 47 | elements=[ 48 | even_bundle, 49 | bundle_with_default 50 | ], 51 | enabled=True 52 | ) 53 | 54 | --- 55 | attachment: 56 | variable name: the_doc[i] 57 | content: | 58 | Test content 59 | --- 60 | mandatory: True 61 | question: | 62 | About to make a PDF 63 | continue button field: the_field 64 | --- 65 | mandatory: True 66 | question: | 67 | Download 68 | subquestion: | 69 | # Should be even 70 | ${ even_bundle.as_pdf(ensure_parity="even") } 71 | 72 | # should be odd 73 | ${ odd_bundle.as_pdf(ensure_parity="odd") } 74 | 75 | # should be even 76 | ${ bundle_with_default.as_pdf() } 77 | 78 | # Should be even 79 | ${ bundle_of_bundles.as_pdf() } -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_familiar_disambiguation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - assembly_line.yml 4 | --- 5 | mandatory: True 6 | code: | 7 | users.gather() 8 | children.gather(number=1) 9 | show_familiar_child 10 | --- 11 | event: show_familiar_child 12 | question: | 13 | We'll call the child "${ children.familiar(unique_names=users) }" -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_question_library.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - assembly_line.yml 4 | --- 5 | metadata: 6 | title: | 7 | Test Question Library 8 | --- 9 | mandatory: True 10 | code: | 11 | which_to_test 12 | if which_to_test == 'name': 13 | users[0].name.first 14 | else: 15 | users[0].name.first = 'John' 16 | users[0].name.last = 'Brown' 17 | 18 | if which_to_test == 'address': 19 | users[0].address.address 20 | if which_to_test == 'address_no_address': 21 | users[0].address.has_no_address 22 | if which_to_test == 'contact': 23 | users[0].phone_number 24 | all_done 25 | --- 26 | event: all_done 27 | question: All done! 28 | --- 29 | question: Which question do you want to test? 30 | field: which_to_test 31 | choices: 32 | - Address: address 33 | - Address (with the option to not have one): address_no_address 34 | - Contact Information: contact 35 | --- 36 | sets: users[0].address.has_no_address 37 | question: | 38 | What is your address? 39 | fields: 40 | - code: | 41 | users[0].address_fields(allow_no_address=True, country_code=AL_DEFAULT_COUNTRY, default_state=AL_DEFAULT_STATE) 42 | 43 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/questions/test_signature.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - assembly_line.yml 4 | --- 5 | code: | 6 | signature_fields = ["users[0].signature"] 7 | --- 8 | mandatory: True 9 | code: | 10 | users.gather() 11 | basic_questions_signature_flow 12 | show_signatures 13 | --- 14 | event: show_signatures 15 | question: | 16 | Your signature 17 | subquestion: | 18 | % if signature_choice == 'sign_after_printing': 19 | You chose to sign after printing. 20 | % else: 21 | Here is your signature: 22 | 23 | ${ users[0].signature } 24 | 25 | % endif 26 | --- 27 | code: | 28 | al_form_requires_digital_signature = False -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/README.md: -------------------------------------------------------------------------------- 1 | # Sources directory 2 | 3 | This directory is used to store word translation files, 4 | machine learning training files, and other source files. 5 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/languages.yml: -------------------------------------------------------------------------------- 1 | # This is a dictionary that contains the native language 2 | # version of a given ISO language code for display in 3 | # the language selection menu. It targets scripts (written language), not spoken. 4 | # It is a limited selection targeted at immigrant populations in the US. 5 | # Please submit a PR if you would like to add a language or a language code. 6 | en: 7 | name: English 8 | native_name: English 9 | es: 10 | name: Spanish 11 | native_name: Español 12 | fr: 13 | name: French 14 | native_name: Français 15 | de: 16 | name: German 17 | native_name: Deutsch 18 | ru: 19 | name: Russian 20 | native_name: Русский 21 | hi: 22 | name: Hindi 23 | native_name: हिन्दी 24 | ar: 25 | name: Arabic 26 | native_name: العربية 27 | ja: 28 | name: Japanese 29 | native_name: 日本語 30 | ko: 31 | name: Korean 32 | native_name: 한국어 33 | vi: 34 | name: Vietnamese 35 | native_name: Tiếng Việt 36 | it: 37 | name: Italian 38 | native_name: Italiano 39 | pt: 40 | name: Portuguese 41 | native_name: Português 42 | tl: 43 | name: Tagalog 44 | native_name: Wikang Tagalog 45 | pl: 46 | name: Polish 47 | native_name: Polski 48 | nl: 49 | name: Dutch 50 | native_name: Nederlands 51 | el: 52 | name: Greek 53 | native_name: Ελληνικά 54 | ht: 55 | name: Haitian Creole 56 | native_name: Kreyòl ayisyen 57 | zh-Hans: 58 | name: Chinese (Simplified) 59 | native_name: 简体中文 60 | zh-Hant: 61 | name: Chinese (Traditional) 62 | native_name: 繁體中文 63 | # If you have a choice, prefer the more specific Chinese (Simplified) or Chinese (Traditional) codes 64 | # Note: because we target scripts, we omit Mandarin/Cantonese distinctions in this file. 65 | zh: 66 | name: Chinese 67 | native_name: 中文 68 | kea: 69 | name: Cape Verdean Creole 70 | native_name: Kriolu 71 | bn: 72 | name: Bengali 73 | native_name: বাংলা 74 | so: 75 | name: Somali 76 | native_name: Soomaali 77 | hy: 78 | name: Armenian 79 | native_name: Հայերեն 80 | hmn: 81 | name: Hmong 82 | native_name: Hmoob 83 | km: 84 | name: Khmer 85 | native_name: ភាសាខ្មែរ 86 | om: 87 | name: Oromo 88 | native_name: Afaan Oromoo 89 | am: 90 | name: Amharic 91 | native_name: አማርኛ 92 | ne: 93 | name: Nepali 94 | native_name: नेपाली 95 | lo: 96 | name: Lao 97 | native_name: ພາສາລາວ 98 | my: 99 | name: Burmese 100 | native_name: ဗမာစာ 101 | ps: 102 | name: Pashto 103 | native_name: پښتو 104 | ku: 105 | name: Kurdish 106 | native_name: Kurdî 107 | ta: 108 | name: Tamil 109 | native_name: தமிழ் 110 | az: 111 | name: Azerbaijani 112 | native_name: Azərbaycan 113 | yo: 114 | name: Yoruba 115 | native_name: Yorùbá 116 | te: 117 | name: Telugu 118 | native_name: తెలుగు 119 | uz: 120 | name: Uzbek 121 | native_name: Oʻzbek 122 | pa: 123 | name: Punjabi 124 | native_name: ਪੰਜਾਬੀ 125 | fa: 126 | name: Farsi (Persian) 127 | native_name: فارسی 128 | sw: 129 | name: Swahili 130 | native_name: Kiswahili 131 | ig: 132 | name: Igbo 133 | native_name: Asụsụ Igbo 134 | ha: 135 | name: Hausa 136 | native_name: هَوُسَ 137 | si: 138 | name: Sinhalese 139 | native_name: සිංහල 140 | gu: 141 | name: Gujarati 142 | native_name: ગુજરાતી 143 | kn: 144 | name: Kannada 145 | native_name: ಕನ್ನಡ 146 | ml: 147 | name: Malayalam 148 | native_name: മലയാളം -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/test_aldocument.feature: -------------------------------------------------------------------------------- 1 | @aldocument 2 | Feature: ALDocument classes 3 | 4 | @aldocs @d1 5 | Scenario: No ALDocument page errors 6 | And the max seconds for each step is 200 7 | Given I start the interview at "test_aldocument" 8 | And I get to "end aldocument tests" with this data: 9 | | var | value | trigger | 10 | | intro_screen | True | | 11 | | as_foo_custom | True | | 12 | | as_foo_defaults | True | | 13 | | download_list_html_custom_1 | True | | 14 | | download_list_html_custom_2 | True | | 15 | | download_list_html_custom_3 | True | | 16 | | download_list_html_defaults | True | | 17 | | email_custom | True | | 18 | | email_defaults | True | | 19 | | x.has_no_file | True | notice_to_quit.has_no_file | 20 | 21 | @alexhibits @e1 22 | Scenario: User can upload exhibits 23 | And the max seconds for each step is 200 24 | Given I start the interview at "test_alexhibit" 25 | And I get to "end alexhibit tests" with this data: 26 | | var | value | trigger | 27 | | x.has_exhibits | True | exhibit_doc_defaults_1.exhibits.has_exhibits | 28 | | x[0].title | Defaults 1 | exhibit_doc_defaults_1.exhibits.has_exhibits | 29 | | x[0].pages | test_alexhibit_docx_1.docx | exhibit_doc_defaults_1.exhibits.has_exhibits | 30 | | x[i].pages.target_number | 2 | exhibit_doc_defaults_1.exhibits[0].pages.there_is_another | 31 | | x[i].pages[j] | test_alexhibit_jpg_1.jpg | exhibit_doc_defaults_1.exhibits[0].pages[1].initialized | 32 | | x.target_number | 2 | exhibit_doc_defaults_1.exhibits.there_is_another | 33 | | x[i].title | Defaults 2 | exhibit_doc_defaults_1.exhibits[1].title | 34 | | x[i].pages | test_alexhibit_pdf_1.pdf | exhibit_doc_defaults_1.exhibits[1].title | 35 | | x[i].pages.target_number | 2 | exhibit_doc_defaults_1.exhibits[1].pages.there_is_another | 36 | | x[i].pages[j] | test_alexhibit_png_1.png | exhibit_doc_defaults_1.exhibits[1].pages[1].initialized | 37 | | x.has_exhibits | True | exhibit_doc_custom_1.exhibits.has_exhibits | 38 | | x[0].title | Defaults 1 | exhibit_doc_custom_1.exhibits.has_exhibits | 39 | | x[0].pages | test_alexhibit_docx_1.docx | exhibit_doc_custom_1.exhibits.has_exhibits | 40 | | x[i].pages.target_number | 2 | exhibit_doc_custom_1.exhibits[0].pages.there_is_another | 41 | | x[i].pages[j] | test_alexhibit_jpg_1.jpg | exhibit_doc_custom_1.exhibits[0].pages[1].initialized | 42 | | x.target_number | 2 | exhibit_doc_custom_1.exhibits.there_is_another | 43 | | x[i].title | Defaults 2 | exhibit_doc_custom_1.exhibits[1].title | 44 | | x[i].pages | test_alexhibit_pdf_1.pdf | exhibit_doc_custom_1.exhibits[1].title | 45 | | x[i].pages.target_number | 2 | exhibit_doc_custom_1.exhibits[1].pages.there_is_another | 46 | | x[i].pages[j] | test_alexhibit_png_1.png | exhibit_doc_custom_1.exhibits[1].pages[1].initialized | 47 | 48 | @alexhibits @e2 49 | Scenario: User adds only a docx exhibit 50 | And the max seconds for each step is 200 51 | Given I start the interview at "test_alexhibit" 52 | And I get to "end alexhibit tests" with this data: 53 | | var | value | trigger | 54 | | x.has_exhibits | True | exhibit_doc_defaults_1.exhibits.has_exhibits | 55 | | x[0].title | DOCX only | exhibit_doc_defaults_1.exhibits.has_exhibits | 56 | | x[0].pages | test_alexhibit_docx_1.docx | exhibit_doc_defaults_1.exhibits.has_exhibits | 57 | | x[i].pages.target_number | 1 | exhibit_doc_defaults_1.exhibits[0].pages.there_is_another | 58 | | x.target_number | 1 | exhibit_doc_defaults_1.exhibits.there_is_another | 59 | | x.has_exhibits | True | exhibit_doc_custom_1.exhibits.has_exhibits | 60 | | x[0].title | DOCX only | exhibit_doc_custom_1.exhibits.has_exhibits | 61 | | x[0].pages | test_alexhibit_docx_1.docx | exhibit_doc_custom_1.exhibits.has_exhibits | 62 | | x[i].pages.target_number | 1 | exhibit_doc_custom_1.exhibits[0].pages.there_is_another | 63 | | x.target_number | 1 | exhibit_doc_custom_1.exhibits.there_is_another | 64 | 65 | @alexhibits @e3 66 | Scenario: User uploads 0 files 67 | And the max seconds for each step is 200 68 | Given I start the interview at "test_alexhibit" 69 | And I get to "end alexhibit tests" with this data: 70 | | var | value | trigger | 71 | | x.has_exhibits | False | exhibit_doc_defaults_1.exhibits.has_exhibits | 72 | | x.has_exhibits | False | exhibit_doc_custom_1.exhibits.has_exhibits | 73 | 74 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/test_alexhibit_docx_1.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/sources/test_alexhibit_docx_1.docx -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/test_alexhibit_jpg_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/sources/test_alexhibit_jpg_1.jpg -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/test_alexhibit_pdf_1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/sources/test_alexhibit_pdf_1.pdf -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/test_alexhibit_png_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/sources/test_alexhibit_png_1.png -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/test_question_library.feature: -------------------------------------------------------------------------------- 1 | @alql 2 | Feature: AL question library 3 | 4 | @alql @ql1 5 | Scenario: Can enter all parts of address 6 | Given the max seconds for each step is 50 7 | And I start the interview at "test_question_library" 8 | Then I set the variable "which_to_test" to "address" 9 | And I tap to continue 10 | Then I should see the phrase "Street address" 11 | And I should see the phrase "Apartment" 12 | And I set the variable "users[0].address.address" to "123 Fake Street" 13 | And I set the variable "users[0].address.unit" to "Apt 5" 14 | And I set the variable "users[0].address.city" to "Boston" 15 | And I set the variable "users[0].address.state" to "MA" 16 | And I set the variable "users[0].address.zip" to "12345" 17 | And I tap to continue 18 | Then I should see the phrase "All done!" 19 | 20 | @alql @ql2 21 | Scenario: Can say no address available 22 | Given the max seconds for each step is 50 23 | And I start the interview at "test_question_library" 24 | Then I set the variable "which_to_test" to "address_no_address" 25 | And I tap to continue 26 | And I set the variable "users[0].address.has_no_address" to "True" 27 | And I set the variable "users[0].address.has_no_address_explanation" to "I sometimes sleep at 5th and Yorkshire" 28 | And I set the variable "users[0].address.city" to "Boston" 29 | And I set the variable "users[0].address.state" to "MA" 30 | And I tap to continue 31 | Then I should see the phrase "All done!" 32 | 33 | @alq1 @ql3 34 | Scenario: Can enter all parts of contact info 35 | Given the max seconds for each step is 50 36 | And I start the interview at "test_question_library" 37 | Then I set the variable "which_to_test" to "contact" 38 | And I tap to continue 39 | And I tap to continue 40 | Then I should see the phrase "You need to provide at least one contact method" 41 | And I set the variable "users[0].mobile_number" to "123-456-7890" 42 | And I tap to continue 43 | Then I should see the phrase "All done!" 44 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/translation_es.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/sources/translation_es.xlsx -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/translation_ht.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/sources/translation_ht.xlsx -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/sources/translation_pt.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/sources/translation_pt.xlsx -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/README.md: -------------------------------------------------------------------------------- 1 | # Static file directory 2 | 3 | If you want to make files available in the web app, put them in 4 | this directory. 5 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/al_audio.css: -------------------------------------------------------------------------------- 1 | /* Only include this style if custom audio element is present */ 2 | .daaudiovideo-control.al_custom_media_controls ~ form .da-page-header { 3 | margin: 20px 0 20px; 4 | } -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/al_audio.js: -------------------------------------------------------------------------------- 1 | // Do not overwrite already created al_js object. This may be relevant as our js gets built out. 2 | if ( !al_js ) { var al_js = {}; } 3 | 4 | $(document).on('daPageLoad', function(event) { 5 | try { 6 | var $audio_nodes = $('.daaudio-control'); 7 | var id_count = 1; 8 | for ( var audio_node of $audio_nodes ) { 9 | // We are not using the returned value right now 10 | al_js.replace_with_audio_minimal_controls( audio_node, 'page_reader_substitute_' + id_count ); 11 | id_count++; 12 | } 13 | } catch ( error ) { console.log( 'AL audio instantiation error:', error, 'event was ', event ) } 14 | }); 15 | 16 | // We are not providing a way to rewind or scan through the audio. 17 | // TODO: Show user the audio still needs to load: 18 | // - https://stackoverflow.com/questions/9337300/html5-audio-load-event, 19 | // - https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play 20 | al_js.replace_with_audio_minimal_controls = function( audio_node, id ) { 21 | /* Create the custom controls for this specific audio element 22 | * and give the new controls container the requested `id` tag. 23 | * Return functions to control audio playback for this node. */ 24 | 25 | var controls = { 26 | play: function () {}, pause: function () {}, 27 | restart: function () {}, stop_and_reset: function () {}, 28 | }; 29 | 30 | if (!audio_node) { 31 | // Some screens, like the signature screen, don't have screen readers. 32 | return controls; 33 | } 34 | 35 | if (audio_node.style.display == "none") { 36 | // The audio node isn't visible. We've likely already replaced it 37 | // with our own stuff, so ignore it. 38 | return; 39 | } 40 | 41 | // Hide the normal controls and show the new controls 42 | audio_node.removeAttribute('controls'); 43 | audio_node.style.display = 'none'; 44 | var $audio_container = $($(audio_node).closest('.daaudiovideo-control')); 45 | $audio_container.addClass( 'al_custom_media_controls al_audio_controls' ); 46 | $('
' + 47 | audio_contents_html + 48 | '
').appendTo( $audio_container ); 49 | 50 | // Start of all the right selectors 51 | var id_s = '#' + id + ' '; 52 | // There are currently two icons displaying at the same time. 53 | // The first is one of play OR pause OR restart. The last is always the stop icon. 54 | $(id_s + '.restart').first().attr('aria-hidden', 'true').hide(); 55 | $(id_s + '.pause').first().attr('aria-hidden', 'true').hide(); 56 | 57 | var is_loading = true; // TODO: Add indication that audio is loading and remove var 58 | controls.play = function () { 59 | try { 60 | audio_node.play().then(function(){ is_loading = false; }); 61 | // pause & stop buttons should be visible 62 | $(id_s + '.pause').first().attr('aria-hidden', 'false').show(); 63 | $(id_s + '.play').first().attr('aria-hidden', 'true').hide(); 64 | $(id_s + '.restart').first().attr('aria-hidden', 'true').hide(); 65 | } catch ( error ) { console.log( 'AL audio play error:', error ) } 66 | } 67 | controls.restart = controls.play; 68 | controls.pause = function() { 69 | try { 70 | audio_node.pause() 71 | // play & stop buttons should be visible 72 | $(id_s + '.play').first().attr('aria-hidden', 'false').show(); 73 | $(id_s + '.restart').first().attr('aria-hidden', 'true').hide(); 74 | $(id_s + '.pause').first().attr('aria-hidden', 'true').hide(); 75 | } catch ( error ) { console.log( 'AL audio pause error:', error ) } 76 | } 77 | controls.stop_and_reset = function () { 78 | try { 79 | audio_node.pause(); 80 | audio_node.currentTime = 0; 81 | $(id_s + '.restart').first().attr('aria-hidden', 'false').show(); 82 | $(id_s + '.play').first().attr('aria-hidden', 'true').hide(); 83 | $(id_s + '.pause').first().attr('aria-hidden', 'true').hide(); 84 | } catch ( error ) { console.log( 'AL audio stop error:', error ) } 85 | } 86 | 87 | $(id_s + '.play').first().on( 'click tap', controls.play ); 88 | $(id_s + '.restart').first().on( 'click tap', controls.restart ); 89 | $(id_s + '.pause').first().on( 'click tap', controls.pause ); 90 | $(id_s + '.stop').first().on( 'click tap', controls.stop_and_reset ); 91 | $(audio_node).on( 'ended', controls.stop_and_reset ); 92 | 93 | // When any button or link is pressed 94 | $('button, a').on( 'click tap', function ( event ) { 95 | try { 96 | // If the button or link is not in an audio control itself, pause the playback 97 | var audio_parent = $(event.target).closest( '.al_audio_controls' ); 98 | if ( audio_parent.length === 0 ) { controls.pause(); } 99 | } catch ( error ) { console.log( 'AL audio button pause error:', error ) } 100 | }); 101 | 102 | // Returns ability to control the playback of these nodes externally 103 | return controls; 104 | }; // Ends al_js.audio_minimal_controls() 105 | 106 | // The DOM structure for every AL audio element with custom controls 107 | var audio_contents_html = '\ 108 | \ 112 | \ 116 | \ 120 | \ 123 | '; 124 | 125 | /* A start on volume functionality: 126 | \ 129 | \ 132 | 133 | // Seems to bottom out at 0 as far as user interaction is concerned 134 | $(id_s +'.volume-down').first().on('click', function(){ audio_node.volume -= 0.1 }); 135 | $(id_s + '.volume-up').first().on('click', function(){ audio_node.volume += 0.1 }); 136 | */ 137 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/aldocument.css: -------------------------------------------------------------------------------- 1 | /* WARNING TO AL DEVELOPERS: Only use CSS _classes_ in our CSS unless you know for a fact that an ID is static. Most IDs are generated dynamically, so using them in the CSS will only work with our test files. */ 2 | 3 | /*---------------------------------------------------------*/ 4 | /* Isolated email send button 5 | /*---------------------------------------------------------*/ 6 | /* Feedback from UI designers has been that email input field and 7 | send button should be on the same line. */ 8 | 9 | .al_send_section_alone { 10 | margin-bottom: 2rem; 11 | } 12 | 13 | .al_send_section_alone .al_email_address, 14 | .al_send_section_alone .al_email_address label, 15 | .al_send_section_alone .al_email_address input { 16 | display: inline; 17 | width: unset; 18 | margin: unset; 19 | } 20 | 21 | .al_send_section_alone .al_email_address input { 22 | max-width: 50% 23 | } 24 | 25 | .al_send_section_alone .al_send_email_button { 26 | flex-grow: 1; 27 | } 28 | 29 | .al_send_section_alone .al_send_editable { 30 | display: block; 31 | width: 100%; 32 | } 33 | 34 | .al_send_section_alone .al_email_container { 35 | display: flex; 36 | justify-content: space-between; 37 | align-items: baseline; 38 | } 39 | 40 | .al_send_section_alone .al_email_address, 41 | .al_send_section_alone .al_email_address label { 42 | max-width: 70%; 43 | padding-left: 0; 44 | } 45 | 46 | .al_send_section_alone .al_email_address input { 47 | max-width: 80%; 48 | } 49 | 50 | .al_send_section_alone .al_doc_email_header { 51 | margin-bottom: 0.2em; 52 | } 53 | 54 | /* al_email_container mobile version */ 55 | @media only screen and (max-width: 700px) { 56 | .al_email_container { 57 | display: block; 58 | } 59 | .al_email_container > span > input { 60 | margin: 0 0 10px; 61 | } 62 | } 63 | 64 | /*---------------------------------------------------------*/ 65 | /* Table rows 66 | /*---------------------------------------------------------*/ 67 | .al_doc_table { 68 | margin-bottom: 2rem; 69 | } 70 | 71 | .al_doc_table_row { 72 | margin-top: 0.5em; 73 | } 74 | 75 | .al_doc_table .al_buttons { 76 | display: flex; 77 | justify-content: space-between; 78 | } 79 | 80 | .al_doc_table .al_buttons > * { 81 | flex-grow: 1; 82 | } 83 | 84 | .al_doc_table .al_buttons .al_button { 85 | margin-right: 4px; 86 | } 87 | 88 | .al_doc_table .al_buttons .al_button:last-child { 89 | margin-right: 0; 90 | } 91 | 92 | .al_doc_table_row .al_doc_title, 93 | .al_doc_table_row .al_buttons, 94 | .al_doc_table_row .al_email_send_col { 95 | text-align: right; 96 | } 97 | 98 | .al_doc_table_row .al_buttons, 99 | .al_doc_table_row .al_email_send_col { 100 | padding-right: 0; 101 | } 102 | 103 | .al_doc_table_row .al_doc_title, 104 | .al_doc_table_row .al_doc_email, 105 | .al_doc_table_row .al_email_input_col { 106 | padding-left: 0; 107 | } 108 | 109 | .al_doc_table_row .al_doc_title { 110 | font-weight: bold; 111 | } 112 | 113 | .al_doc_table_row .al_email_input_container { 114 | display: flex; 115 | justify-content: space-between; 116 | } 117 | 118 | .al_doc_table_row .al_email_input_container label { 119 | width: unset; 120 | padding-right: 1em; 121 | } 122 | 123 | .al_doc_table .al_email_input_container { 124 | flex-wrap: wrap; 125 | } 126 | 127 | .al_doc_table_row .al_doc_email_field { 128 | width: 50%; 129 | flex-grow: 1; 130 | } 131 | 132 | @media (max-width: 575px) { 133 | .al_doc_table .col { 134 | padding-right: 0; 135 | padding-left: 0; 136 | } 137 | .al_doc_table .al_doc_title { 138 | text-align: left; 139 | } 140 | .al_doc_table .al_email_input_col { 141 | padding-bottom: 0.5em; 142 | padding-top: 0.25em; 143 | } 144 | } 145 | /* TODO: Should it be in here and this broad? This looks like it's for all pages */ 146 | .da-subquestion .daquestionbackbutton { 147 | background-color: #eee; 148 | } 149 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/aldocument.js: -------------------------------------------------------------------------------- 1 | /* 2 | * given a docassemble event name, and text ID containing email address and whether 3 | * or not user wants to send editable files, trigger docassemble event to 4 | * email an ALDocumentBundle 5 | */ 6 | function aldocument_send_action(event_name, wants_editable_id, email_id, template_name="", key="final") { 7 | var editable = false; 8 | 9 | console.log( email_id ); 10 | var editable_choice_node = $('#' + wants_editable_id); 11 | if ( editable_choice_node && editable_choice_node[0] ) { 12 | editable = $('#' + wants_editable_id)[0].checked; 13 | } 14 | 15 | var email = $('#' + email_id)[0].value; 16 | da_action_perform(event_name, {editable: editable, email: email, key:key, template_name: template_name}); 17 | }; 18 | 19 | /* 20 | * given a docassemble event name, and text ID containing email address and whether 21 | * or not user wants to send editable files, trigger docassemble event to 22 | * email an ALDocumentBundle 23 | */ 24 | function aldocument_send_to_action(event_name, editable, email, button_id, template_name="", key="final") { 25 | var button = $('#' + button_id); 26 | button.html('Sending...'); 27 | button.addClass('disabled'); // to visually disable button 28 | da_action_call(event_name, {editable: editable, email: email, template_name: template_name, key:key}, function (results) { 29 | if (results.success) { 30 | console.log("Email sent") 31 | button.prop('disabled', true); 32 | button.html('Sent'); 33 | } else { 34 | console.log("Email failed") 35 | button.prop('disabled', false); 36 | button.html('Send'); 37 | button.removeClass('disabled'); // to visually enable 38 | } 39 | }); 40 | }; -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/interview_list.css: -------------------------------------------------------------------------------- 1 | .daclickable.nav-link.danavlink:hover { 2 | border: 1px solid; 3 | } 4 | 5 | /* https://codepen.io/dragontheory/pen/qvJYzz */ 6 | @import url('https://fonts.googleapis.com/css?family=Raleway:400,500,600,700,800,900'); 7 | 8 | /* article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { 9 | display: block; 10 | }*/ 11 | 12 | /*.al-progress-box { 13 | display: block; 14 | box-sizing: border-box; 15 | font-family: "Raleway",sans-serif; 16 | text-align:center; 17 | display: -webkit-box; 18 | display: -ms-flexbox; 19 | display: flex; 20 | -ms-flex-flow: row wrap; 21 | flex-flow: row wrap; 22 | }*/ 23 | 24 | .al-session-form-title { 25 | font-weight: bolder; 26 | } 27 | 28 | .radialProgressBar { 29 | border-radius: 50%; 30 | width: 2.5rem; 31 | height: 2.5rem; 32 | display: -webkit-box; 33 | display: -ms-flexbox; 34 | display: flex; 35 | --radial-background: #ddd; 36 | background: var(--radial-background); 37 | margin: .2rem; 38 | } 39 | 40 | [data-bs-theme=dark] .radialProgressBar { 41 | --radial-background: #212529; 42 | } 43 | 44 | .radialProgressBar .overlay { 45 | border-radius: 50%; 46 | width: 2.3rem; 47 | height: 2.3rem; 48 | margin: auto; 49 | background: #fff; 50 | text-align: center; 51 | padding-top: 10%; 52 | } 53 | 54 | [data-bs-theme=dark] .radialProgressBar .overlay { 55 | background: #212529; 56 | } 57 | 58 | .progress-0 { 59 | background-image: -webkit-linear-gradient(left, var(--radial-background) 50%, transparent 50%), -webkit-linear-gradient(left, #028cd5 50%, var(--radial-background) 50%); 60 | background-image: linear-gradient(90deg, var(--radial-background) 50%, transparent 50%), linear-gradient(90deg, #028cd5 50%, var(--radial-background) 50%); 61 | } 62 | 63 | .progress-10 { 64 | background-image: -webkit-linear-gradient(36deg, var(--radial-background) 50%, transparent 50%), -webkit-linear-gradient(left, #028cd5 50%, var(--radial-background) 50%); 65 | background-image: linear-gradient(54deg, var(--radial-background) 50%, transparent 50%), linear-gradient(90deg, #028cd5 50%, var(--radial-background) 50%); 66 | } 67 | 68 | .progress-20 { 69 | background-image: -webkit-linear-gradient(72deg, var(--radial-background) 50%, transparent 50%), -webkit-linear-gradient(left, #028cd5 50%, var(--radial-background) 50%); 70 | background-image: linear-gradient(18deg, var(--radial-background) 50%, transparent 50%), linear-gradient(90deg, #028cd5 50%, var(--radial-background) 50%); 71 | } 72 | 73 | .progress-30 { 74 | background-image: -webkit-linear-gradient(108deg, var(--radial-background) 50%, transparent 50%), -webkit-linear-gradient(left, #028cd5 50%, var(--radial-background) 50%); 75 | background-image: linear-gradient(-18deg, var(--radial-background) 50%, transparent 50%), linear-gradient(90deg, #028cd5 50%, var(--radial-background) 50%); 76 | } 77 | 78 | .progress-40 { 79 | background-image: -webkit-linear-gradient(144deg, var(--radial-background) 50%, transparent 50%), -webkit-linear-gradient(left, #028cd5 50%, var(--radial-background) 50%); 80 | background-image: linear-gradient(-54deg, var(--radial-background) 50%, transparent 50%), linear-gradient(90deg, #028cd5 50%, var(--radial-background) 50%); 81 | } 82 | 83 | .progress-50 { 84 | background-image: -webkit-linear-gradient(right, var(--radial-background) 50%, transparent 50%), -webkit-linear-gradient(left, #028cd5 50%, var(--radial-background) 50%); 85 | background-image: linear-gradient(-90deg, var(--radial-background) 50%, transparent 50%), linear-gradient(90deg, #028cd5 50%, var(--radial-background) 50%); 86 | } 87 | 88 | .progress-60 { 89 | background-image: -webkit-linear-gradient(left, #028cd5 50%, transparent 50%), -webkit-linear-gradient(36deg, #028cd5 50%, var(--radial-background) 50%); 90 | background-image: linear-gradient(90deg, #028cd5 50%, transparent 50%), linear-gradient(54deg, #028cd5 50%, var(--radial-background) 50%); 91 | } 92 | 93 | .progress-70 { 94 | background-image: -webkit-linear-gradient(left, #028cd5 50%, transparent 50%), -webkit-linear-gradient(72deg, #028cd5 50%, var(--radial-background) 50%); 95 | background-image: linear-gradient(90deg, #028cd5 50%, transparent 50%), linear-gradient(18deg, #028cd5 50%, var(--radial-background) 50%); 96 | } 97 | 98 | .progress-80 { 99 | background-image: -webkit-linear-gradient(left, #028cd5 50%, transparent 50%), -webkit-linear-gradient(108deg, #028cd5 50%, var(--radial-background) 50%); 100 | background-image: linear-gradient(90deg, #028cd5 50%, transparent 50%), linear-gradient(-18deg, #028cd5 50%, var(--radial-background) 50%); 101 | } 102 | 103 | .progress-90 { 104 | background-image: -webkit-linear-gradient(left, #028cd5 50%, transparent 50%), -webkit-linear-gradient(144deg, #028cd5 50%, var(--radial-background) 50%); 105 | background-image: linear-gradient(90deg, #028cd5 50%, transparent 50%), linear-gradient(-54deg, #028cd5 50%, var(--radial-background) 50%); 106 | } 107 | 108 | .progress-100 { 109 | background-image: -webkit-linear-gradient(left, #028cd5 50%, transparent 50%), -webkit-linear-gradient(right, #028cd5 50%, var(--radial-background) 50%); 110 | background-image: linear-gradient(90deg, #028cd5 50%, transparent 50%), linear-gradient(-90deg, #028cd5 50%, var(--radial-background) 50%); 111 | } 112 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/limit_upload_size.js: -------------------------------------------------------------------------------- 1 | // Limit upload size of file 2 | // Initial version: @plocket 3 | 4 | // TODO: Explore CustomDatatype js instead of the js in here 5 | // TODO: figure out a performant way to make upload size configurable 6 | 7 | var dom_data = {}; 8 | 9 | var clear_error = function () { 10 | /* Removes error message, enables continue button. */ 11 | var $error = $('.da-field-container-datatype-files label.da-has-error, .da-field-container-datatype-file label.da-has-error'); 12 | // We can fiddle with this behavior if desired. 13 | $error.css( 'display', 'none' ); 14 | $error.text( '' ); 15 | 16 | // re-enable continue button (assumes continue button) 17 | var next = $('.da-field-buttons button[type="submit"]')[0]; 18 | next.disabled = false; 19 | 20 | // Clear listeners so they don't pile up 21 | $('.fileinput-remove').off( 'click', clear_error ); 22 | }; // Ends clear_error() 23 | 24 | var prevent_big_files = function () { 25 | /* If the size of uploaded files is too big, show an 26 | * error and don't allow the user to continue. */ 27 | // TODO: investigate a reasonable way to make upload size configurable 28 | var max_mb = 25;// HARDCODED for performance -- replaces var_data.variables.max_mb_per_file; 29 | var max_bytes = max_mb * 1024 * 1024; 30 | var files = dom_data.upload_node.files 31 | 32 | var total_size = 0; 33 | for ( var file_i = 0; file_i < files.length; file_i++ ) { 34 | total_size += files[ file_i ].size; 35 | } 36 | 37 | if ( total_size > max_bytes ) { 38 | var $error = $('.da-field-container-datatype-files label.da-has-error, .da-field-container-datatype-file label.da-has-error'); 39 | $error.css( 'display', 'inline-block' ); 40 | // TODO: make this translatable 41 | $error.text( 'You cannot upload files that are larger than ' + max_mb + ' MB' ); 42 | 43 | // Disable continuing to next page. 44 | // Assumes there's a continue button 45 | var next = $('.da-field-buttons button[type="submit"]')[0]; 46 | next.disabled = true; 47 | 48 | // Listen for removal of file 49 | $('.fileinput-remove').on( 'click', clear_error ); 50 | 51 | // Otherwise make sure the error state stuff is removed/not present 52 | } else { clear_error(); } 53 | }; // Ends prevent_big_files() 54 | 55 | 56 | 57 | $( document ).ready(function () { 58 | // When the document is ready, start listening for the relevant events 59 | 60 | // The file input field itself changing (getting a value) 61 | $('.dafile').on( 'change', function (evnt) { 62 | dom_data.upload_node = evnt.target; 63 | prevent_big_files(); 64 | }); 65 | 66 | // The drag and drop area getting a file dropped 67 | $(document.body).on('drop', function (event) { 68 | var $dafiles = $('.dafile'); 69 | if ( $dafiles ) { 70 | var $dafile = $dafiles[0]; 71 | if ( $dafile ) { 72 | if (Array.isArray($dafile)) { 73 | dom_data.upload_node = $dafile[0]; 74 | } else { 75 | dom_data.upload_node = $dafile; 76 | } 77 | prevent_big_files(); 78 | } 79 | } 80 | }); 81 | 82 | }); // ends on document ready 83 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/lt1-pullout-10-getting-organized.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/static/lt1-pullout-10-getting-organized.pdf -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/ma_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/static/ma_logo.png -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/static/placeholder.png -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/seal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/static/seal.jpg -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/static/styles.css: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------*/ 2 | /* Logo and title 3 | /*---------------------------------------------------------*/ 4 | 5 | .title-container { 6 | display: grid; 7 | grid-template-columns: min-content min-content; 8 | grid-template-rows: min-content; 9 | gap: 0px 0.5em; 10 | grid-template-areas: 11 | "logo title"; 12 | justify-items: start; 13 | align-items: center; 14 | } 15 | .al-logo { 16 | grid-area: logo; 17 | } 18 | .al-logo img { 19 | max-height: 40px; 20 | } 21 | .al-title { 22 | display: grid; 23 | grid-template-columns: min-content; 24 | grid-template-rows: 1fr 1fr; 25 | gap: 0px 0px; 26 | grid-template-areas: 27 | "title-row-1" 28 | "title-row-2"; 29 | justify-content: start; 30 | align-content: start; 31 | justify-items: start; 32 | align-items: start; 33 | grid-area: title; 34 | line-height: 1em; 35 | font-family: 'Inter Bold', 'Arial'; 36 | } 37 | 38 | .title-row-1 { 39 | grid-area: title-row-1; 40 | font-weight: bold; 41 | font-size: .7em; 42 | text-transform: none; /*prevent any bootstrap theme to transform it to uppercase */ 43 | } 44 | 45 | .title-row-2 { 46 | grid-area: title-row-2; 47 | font-weight: bold; 48 | font-size: 1em; 49 | text-transform: none; /*prevent any bootstrap theme to transform it to uppercase */ 50 | } 51 | 52 | @media only screen and (max-width: 800px) { 53 | .al-title, .al-logo { 54 | font-weight: normal; 55 | font-size: 60%; 56 | } 57 | .al-logo { 58 | border-width: 4px; 59 | } 60 | } 61 | 62 | .danavbar-title { max-width: unset } 63 | 64 | /*---------------------------------------------------------*/ 65 | /* Navigation bar and footer 66 | /*---------------------------------------------------------*/ 67 | /*.navbar { 68 | height: 60px; 69 | } 70 | */ 71 | .da-pad-for-navbar { 72 | padding-top: 70px; 73 | } 74 | .da-top-for-navbar { 75 | top: 70px; 76 | } 77 | footer { 78 | text-align: right 79 | } 80 | footer a { 81 | padding-left: 2em; 82 | } 83 | footer a:first-child { 84 | padding-left: 0em; 85 | } 86 | /*---------------------------------------------------------*/ 87 | /* Content 88 | /*---------------------------------------------------------*/ 89 | blockquote { 90 | background: #f9f9f9; 91 | border-left: 10px solid #ccc; 92 | margin: 1.5em 10px; 93 | padding: 0.5em 10px; 94 | quotes: "\201C""\201D""\2018""\2019"; 95 | font-size: .75em; 96 | } 97 | blockquote:before { 98 | color: #ccc; 99 | content: open-quote; 100 | font-size: 4em; 101 | line-height: 0.1em; 102 | margin-right: 0.25em; 103 | vertical-align: -0.4em; 104 | } 105 | blockquote p { 106 | display: inline; 107 | } 108 | /* [FILE] image shadow */ 109 | .daicon { 110 | box-shadow: 5px 5px 15px #69868e; 111 | border-style: solid; 112 | border-width: 1px; 113 | border-color: gray; 114 | } 115 | 116 | td img.daicon { 117 | width: 100%; 118 | } 119 | 120 | /* Reduce integer and number input box size */ 121 | input[type=number] { 122 | width: 10rem; 123 | } 124 | /* Widen the Submit button (button labels vary) */ 125 | .btn[type='submit'] { 126 | min-width: 8em; 127 | } 128 | 129 | /* Make the footer links and da terms slightly darker in light mode 130 | and lighter in dark mode for better contrast */ 131 | footer a { 132 | color: #0264F7; 133 | } 134 | 135 | [data-bs-theme=dark] footer a { 136 | color: #3988FF 137 | } 138 | 139 | .daterm { 140 | color: #3b842c; 141 | text-decoration: dashed; 142 | text-decoration-line: underline; 143 | } 144 | 145 | [data-bs-theme=dark] .daterm { 146 | color: #4fa23e; 147 | } 148 | 149 | .daquestionbackbutton { 150 | --bs-btn-focus-shadow-rgb: 49,132,253; 151 | } 152 | 153 | /* An upstream fix for dark mode, with radio buttons and checkboxes. See https://github.com/jhpyle/docassemble/pull/718 */ 154 | [data-bs-theme=dark] .btn-light.dalabelauty { 155 | --bs-btn-color: #fff; 156 | --bs-btn-bg: #212529; 157 | --bs-btn-border-color: #212529; 158 | --bs-btn-hover-color: #fff; 159 | --bs-btn-hover-bg: #424649; 160 | --bs-btn-hover-border-color: #373b3e; 161 | --bs-btn-focus-shadow-rgb: 66, 70, 73; 162 | --bs-btn-active-color: #fff; 163 | --bs-btn-active-bg: #4d5154; 164 | --bs-btn-active-border-color: #373b3e; 165 | --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 166 | --bs-btn-disabled-color: #fff; 167 | --bs-btn-disabled-bg: #212529; 168 | --bs-btn-disabled-border-color: #212529; 169 | } 170 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/templates/README.md: -------------------------------------------------------------------------------- 1 | # Template directory 2 | 3 | If you want to use templates for document assembly, put them in this directory. 4 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/templates/al_basic_addendum.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/templates/al_basic_addendum.docx -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/templates/exhibit_cover_sheet.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/templates/exhibit_cover_sheet.docx -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/templates/exhibit_table_of_contents.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/templates/exhibit_table_of_contents.docx -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/templates/general_cover_sheet.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/templates/general_cover_sheet.docx -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/templates/sample_word_template.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/templates/sample_word_template.docx -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/templates/test_aldocument_docx_1.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/templates/test_aldocument_docx_1.docx -------------------------------------------------------------------------------- /docassemble/AssemblyLine/data/templates/test_aldocument_pdf_1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/data/templates/test_aldocument_pdf_1.pdf -------------------------------------------------------------------------------- /docassemble/AssemblyLine/language.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | from typing import List, Optional, Tuple 4 | from docassemble.base.util import url_action, path_and_mimetype 5 | import yaml 6 | import pycountry 7 | 8 | __all__ = [ 9 | "get_tuples", 10 | "get_language_list_dropdown", 11 | "get_language_list_dropdown_item", 12 | "get_language_list", 13 | "get_language_list_item", 14 | ] 15 | 16 | 17 | def _package_name(package_name: Optional[str] = None) -> str: 18 | """Get package name without the name of the given module. By default this is `docassemble.AssemblyLine.language` 19 | 20 | Args: 21 | package_name: the name of the package to get the package name from (defaults to `docassemble.AssemblyLine.language`) 22 | 23 | Returns: 24 | str: the package name without the name of the given module 25 | """ 26 | if not package_name: 27 | package_name = __name__ 28 | try: 29 | return ".".join(package_name.split(".")[:-1]) 30 | except: 31 | return package_name 32 | 33 | 34 | def get_local_languages_yaml() -> str: 35 | """ 36 | Get the path to the local languages.yml file. If it does not exist, it will return the path to the languages.yml 37 | 38 | Returns: 39 | str: the path to the local languages.yml file if it exists, otherwise the path to the languages.yml file 40 | """ 41 | try: 42 | local_yaml = path_and_mimetype("data/sources/languages.yml")[0] 43 | except: 44 | local_yaml = None 45 | 46 | if local_yaml and os.path.isfile(local_yaml): 47 | return local_yaml 48 | 49 | al_package_name = _package_name() 50 | this_yaml = path_and_mimetype(f"{al_package_name}:data/sources/languages.yml")[0] 51 | return this_yaml 52 | 53 | 54 | def get_tuples( 55 | lang_codes: List[str], languages_path: Optional[str] = None 56 | ) -> List[Tuple[str, str]]: 57 | """ 58 | Returns a list of tuples representing the language name, followed by language ISO 639-1 code. 59 | 60 | It will use the native_name value from the languages.yml file if available, otherwise it will use the 61 | English name from pycountry. If neither is present, it will use the language code itself. 62 | 63 | Args: 64 | lang_codes: a list of ISO 639-1 language codes (e.g. ['en', 'es']) 65 | languages_path: the path to the languages.yml file (defaults to data/sources/languages.yml) 66 | 67 | Returns: 68 | A list of tuples representing the language name, followed by language ISO 639-1 code. 69 | 70 | """ 71 | if not languages_path: 72 | languages_path = get_local_languages_yaml() 73 | 74 | if languages_path is not None: 75 | with open(languages_path, "r", encoding="utf-8") as stream: 76 | # use our hand-built dictionary matching languages to native name for languages 77 | # if it is available 78 | languages = yaml.safe_load(stream) 79 | tuples = [] 80 | for lang in lang_codes: 81 | # English name 82 | try: 83 | english_name = pycountry.languages.get(alpha_2=lang).name 84 | except: 85 | english_name = lang 86 | tuples.append((languages[lang].get("native_name", english_name), lang)) 87 | return tuples 88 | else: 89 | # Get the english name for the language code from pycountry as a fallback 90 | return [ 91 | ( 92 | ( 93 | pycountry.languages.get(alpha_2=lang).name 94 | if pycountry.languages.get(alpha_2=lang) 95 | else lang 96 | ), 97 | lang, 98 | ) 99 | for lang in lang_codes 100 | ] 101 | 102 | 103 | def get_language_list_dropdown( 104 | lang_codes: List[str], 105 | current: str = "", 106 | languages_path: Optional[str] = None, 107 | event_name="al_change_language", 108 | icon="fa-solid fa-language fa-xl", 109 | extra_class: str = "text-light", 110 | ) -> str: 111 | """ 112 | Get a Bootstrap 5 dropdown menu for language selection that can be added to navigation bar. 113 | 114 | Args: 115 | lang_codes: a list of ISO 639-1 language codes (e.g. ['en', 'es']) 116 | current: the current language code 117 | languages_path: the path to the languages.yml file (defaults to data/sources/languages.yml) 118 | event_name: the name of the event to trigger when the language is changed 119 | icon: the name of the icon to use for the dropdown menu (defaults to fa-solid fa-language fa-xl) 120 | extra_class: additional classes to add to the link 121 | Returns: 122 | A string containing the HTML for a dropdown menu for language selection. 123 | """ 124 | list_start = f""" 133 | """ 134 | if not languages_path: 135 | languages_path = get_local_languages_yaml() 136 | languages = get_tuples(lang_codes, languages_path=languages_path) 137 | 138 | for language in languages: 139 | if language[1] == current: 140 | list_start += get_language_list_dropdown_item( 141 | language, link=False, event_name=event_name 142 | ) 143 | else: 144 | list_start += get_language_list_dropdown_item( 145 | language, event_name=event_name 146 | ) 147 | return list_start + list_end 148 | 149 | 150 | def get_language_list_dropdown_item( 151 | language: Tuple[str, str], link: bool = True, event_name="al_change_language" 152 | ) -> str: 153 | """Given an ordered tuple, returns a link to the current interview with lang=language code and the link title 154 | given in the first part of the tuple. 155 | 156 | Args: 157 | language: a tuple containing the language name and language code 158 | link: whether to return a link or just the text 159 | event_name: the name of the event to trigger when the language is changed 160 | 161 | Returns: 162 | str: A string containing the HTML for a dropdown menu item for language selection. 163 | """ 164 | 165 | if link: 166 | return f"""{ language[0] }""" 167 | else: 168 | return f'{ language[0] }' 169 | 170 | 171 | def get_language_list( 172 | languages: Optional[List[Tuple[str, str]]] = None, 173 | current="", 174 | lang_codes: Optional[List[str]] = None, 175 | languages_path: Optional[str] = None, 176 | event_name="al_change_language", 177 | ) -> str: 178 | """ 179 | Given a list of language codes, returns 180 | a Bootstrap-formatted unordered inline list. The current language will not be a link. 181 | 182 | Deprecated behavior: instead of a list of language codes, you can provide list of 183 | tuples containing the language name and language code. This is deprecated and may be removed in a future version. 184 | 185 | Args: 186 | languages: a list of tuples containing the language name and language code (deprecated) 187 | current: the current language code 188 | lang_codes: a list of ISO 639-1 language codes (e.g. ['en', 'es']) 189 | languages_path: the path to the languages.yml file (defaults to data/sources/languages.yml) 190 | event_name: the name of the event to trigger when the language is changed 191 | 192 | Returns: 193 | A string containing the HTML for an unordered inline list of language selection. 194 | """ 195 | if not languages_path: 196 | languages_path = get_local_languages_yaml() 197 | if not languages: 198 | if not lang_codes: 199 | raise ValueError("Either languages or lang_codes must be provided") 200 | languages = get_tuples(lang_codes, languages_path=languages_path) 201 | 202 | list_start = '
    ' 203 | list_start += '
  • Language:
  • ' 204 | list_end = "
" 205 | for language in languages: 206 | if language[1] == current: 207 | list_start += get_language_list_item( 208 | language, link=False, event_name=event_name 209 | ) 210 | else: 211 | list_start += get_language_list_item(language, event_name=event_name) 212 | return list_start + list_end 213 | 214 | 215 | def get_language_list_item(language, link=True, event_name="al_change_language") -> str: 216 | """Given an ordered tuple, returns a link to the current interview with lang=language code and the link title 217 | given in the first part of the tuple. 218 | 219 | Args: 220 | language: a tuple containing the language name and language code 221 | link: whether to return a link or just the text 222 | event_name: the name of the event to trigger when the language is changed 223 | 224 | Returns: 225 | str: A string containing the HTML for an unordered inline list item for language selection. 226 | """ 227 | li_start = '
  • ' 228 | li_end = "
  • " 229 | 230 | if link: 231 | iurl = url_action(event_name, lang=language[1]) 232 | return ( 233 | li_start 234 | + '' 237 | + language[0] 238 | + "" 239 | + li_end 240 | ) 241 | else: 242 | return li_start + language[0] + li_end 243 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuffolkLITLab/docassemble-AssemblyLine/0a90dfe49695c2dec7d72c64a2960fb68f7f9c94/docassemble/AssemblyLine/py.typed -------------------------------------------------------------------------------- /docassemble/AssemblyLine/requirements.txt: -------------------------------------------------------------------------------- 1 | docassemble.base>=1.3 2 | docassemble.webapp 3 | mypy 4 | pandas-stubs 5 | sqlalchemy[mypy] 6 | types-PyYAML 7 | PyGithub 8 | types-requests 9 | types-psycopg2 -------------------------------------------------------------------------------- /docassemble/AssemblyLine/sign.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | import os 3 | from typing import Optional, List, Union 4 | from docassemble.base.util import log 5 | 6 | 7 | def find_font_file_by_name(font_name: str, search_dirs: List[str]) -> Optional[str]: 8 | """ 9 | Recursively search for a font file by name in the specified directories. 10 | 11 | This function appends '.ttf' to the font name if it is not already present and 12 | searches through each directory in `search_dirs` recursively for a file that 13 | matches the font name (case-insensitive). 14 | 15 | Args: 16 | font_name (str): The name of the font to search for (without path). 17 | search_dirs (List[str]): A list of directories to search. 18 | 19 | Returns: 20 | Optional[str]: The full path to the font file if found; otherwise, None. 21 | """ 22 | # Append '.ttf' if necessary. 23 | if not font_name.lower().endswith(".ttf"): 24 | target_font = font_name + ".ttf" 25 | else: 26 | target_font = font_name 27 | 28 | for directory in search_dirs: 29 | if os.path.exists(directory): 30 | for root, _, files in os.walk(directory): 31 | for file in files: 32 | if file.lower() == target_font.lower(): 33 | return os.path.join(root, file) 34 | return None 35 | 36 | 37 | def get_font( 38 | font_name: Optional[str] = None, font_size: int = 48 39 | ) -> Union[ImageFont.ImageFont, ImageFont.FreeTypeFont]: 40 | """ 41 | Loads a font by name from candidate directories and returns an ImageFont instance. 42 | 43 | If `font_name` is provided, it may be either a full path to a font file or a font 44 | name (without a path). In the latter case, the function will search for the font in 45 | `/var/www/.fonts` and `/usr/share/fonts/truetype/`, automatically appending '.ttf' 46 | if necessary. If no font is found, the function falls back to the default Pillow font. 47 | 48 | Args: 49 | font_name (Optional[str]): The desired font's full path or name. Defaults to None. 50 | font_size (int): The size of the font to be used. Defaults to 48. 51 | 52 | Returns: 53 | ImageFont.ImageFont: The loaded font instance. 54 | """ 55 | search_dirs: List[str] = ["/var/www/.fonts", "/usr/share/fonts/truetype"] 56 | 57 | if font_name: 58 | # If a full path is provided and exists, use it. 59 | if os.path.isabs(font_name) and os.path.exists(font_name): 60 | try: 61 | return ImageFont.truetype(font_name, font_size) 62 | except Exception as e: 63 | print(f"Error loading provided font {font_name}: {e}") 64 | else: 65 | # Try to find the font by name in the candidate directories. 66 | found_font = find_font_file_by_name(font_name, search_dirs) 67 | if found_font: 68 | try: 69 | return ImageFont.truetype(found_font, font_size) 70 | except Exception as e: 71 | log(f"Error loading font {found_font} found for {font_name}: {e}") 72 | else: 73 | log(f"Font '{font_name}' not found in candidate directories.") 74 | else: 75 | # No font specified; try default candidate font names. 76 | candidate_font_names: List[str] = [ 77 | "BadScript-Regular", 78 | "arial", 79 | "times", 80 | "DejaVuSans", 81 | ] 82 | for candidate in candidate_font_names: 83 | found_font = find_font_file_by_name(candidate, search_dirs) 84 | if found_font: 85 | try: 86 | return ImageFont.truetype(found_font, font_size) 87 | except Exception as e: 88 | log(f"Error loading candidate font {found_font}: {e}") 89 | 90 | # Final fallback to Pillow's default font. 91 | log("Falling back to the default Pillow font.") 92 | return ImageFont.load_default() 93 | 94 | 95 | def create_signature( 96 | name: str, 97 | output_file: str, 98 | signature_prefix: str = "/s/", 99 | font_name: Optional[str] = None, 100 | font_size: int = 48, 101 | ) -> None: 102 | """ 103 | Creates an image file that simulates a signature. 104 | 105 | The signature image is generated by combining the provided `signature_prefix` 106 | (for example, "/s/" or "s/") with the person's name. The text size is measured to 107 | create an image with appropriate dimensions, and then the text is drawn onto a 108 | white background. The final image is saved to the specified output file path. 109 | 110 | Args: 111 | name (str): The person's name to be signed. 112 | output_file (str): The full file path where the image will be written. 113 | signature_prefix (str): The signature prefix to use (e.g., "/s/" or "s/"). Defaults to "/s/". 114 | font_name (Optional[str]): The font's full path or name (without path). Defaults to None. 115 | font_size (int): The size of the font to be used. Defaults to 48. 116 | """ 117 | # Construct the signature text. 118 | signature_text = f"{signature_prefix} {name}" if signature_prefix.strip() else name 119 | 120 | # Load the font using the provided font name or default candidate fonts. 121 | font = get_font(font_name, font_size) 122 | 123 | # Create a temporary image to measure text size. 124 | dummy_img = Image.new("RGB", (1, 1)) 125 | dummy_draw = ImageDraw.Draw(dummy_img) 126 | 127 | bbox = dummy_draw.textbbox((0, 0), signature_text, font=font) 128 | text_width = bbox[2] - bbox[0] 129 | text_height = bbox[3] - bbox[1] 130 | 131 | # Define margins around the text. 132 | margin = 20 133 | img_width = text_width + 2 * margin 134 | img_height = text_height + 2 * margin 135 | 136 | # Create the final image with a white background. 137 | img = Image.new("RGB", (int(img_width), int(img_height)), "white") 138 | draw = ImageDraw.Draw(img) 139 | 140 | # Draw the signature text onto the image. 141 | draw.text((margin, margin), signature_text, font=font, fill="black") 142 | 143 | # Save the image to the specified output file path. 144 | img.save(output_file, format="PNG") 145 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/test_al_document.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from docassemble.base.util import DAFile 3 | from .al_document import ALDocument, ALDocumentBundle, ALAddendumField 4 | 5 | 6 | class test_dont_assume_pdf(unittest.TestCase): 7 | def test_upload_pdf(self): 8 | doc1 = ALDocument( 9 | "doc1", 10 | title="PDF 1", 11 | filename="pdf_doc_1", 12 | enabled=True, 13 | has_addendum=False, 14 | ) 15 | doc1["final"] = DAFile("test_aldocument_pdf_1.pdf") 16 | doc2 = ALDocument( 17 | "doc2", 18 | title="DOCX 2", 19 | filename="docx_doc_1", 20 | enabled=True, 21 | has_addendum=False, 22 | ) 23 | doc2["final"] = DAFile("test_aldocument_docx_1.docx") 24 | al_doc_bundle = ALDocumentBundle( 25 | "al_doc_bundle", 26 | elements=[doc1, doc2], 27 | title="Multiple docs", 28 | filename="multi_bundle_1", 29 | enabled=True, 30 | ) 31 | new_list = al_doc_bundle.as_editable_list() 32 | self.assertEqual(len(new_list), 2) 33 | pass 34 | 35 | 36 | class test_aladdendum(unittest.TestCase): 37 | def test_safe_value(self): 38 | text_testcase1 = """Charged by my father with a very delicate mission, I repaired, towards the end of May, 1788, to the château of Ionis, situated a dozen leagues distant, in the lands lying between Angers and Saumur. I was twenty-two, and already practising the profession of lawyer, for which I experienced but slight inclination, although neither the study of business nor of argument had presented serious difficulties to me. Taking my youth into consideration, I was not esteemed without talent, and the standing of my father, a lawyer renowned in the locality, assured me a brilliant patronage in the future, in return for any paltry efforts I might make to be worthy of replacing him. But I would have preferred literature, a more dreamy life, a more independent and more individual use of my faculties, a responsibility less submissive to the passions and interests of others. As my family was well off, and I an only son, greatly spoiled and petted, I might have chosen my own career, but I would have thus afflicted my father, who took pride in his ability to direct me 4in the road which he had cleared in advance, and I loved him too tenderly to permit my instinct to outweigh his wishes. 39 | 40 | It was a delightful evening in which I was finishing my ride on horseback through the woods that surrounded the ancient and magnificent castle of Ionis. I was well mounted, dressed en cavalier, with a species of elegance, and accompanied by a servant of whom I had not the slightest need, but whom my mother had conceived the innocent idea of giving me for the occasion, desiring that her son should present a proper appearance at the house of one of the most brilliant personages of our patronage. 41 | 42 | The night was illuminated by the soft fire of its largest stars. A slight mist veiled the scintillations of those myriads of satellites that gleam like brilliant eyes on clear, cold evenings. This was a true summer sky, pure enough to be luminous and transparent, still sufficiently softened not to overwhelm one by its immeasurable wealth. It was, if I may so speak, one of those soft firmaments that permit one to think of earth, to admire the vaporous lines of narrow horizons, to breathe without disdain its atmosphere of flowers and herbage—in fine, to consider oneself as something in this immensity, and to forget that one is but an atom in the infinite. 43 | 44 | In proportion as I approached the seigneurial park the wild perfumes of the forest were mingled with those of the lilacs and acacias, whose blooming heads leaned over the wall. Soon through the shrubbery I saw the windows of the manor gleaming behind their curtains of purple moire, divided by the dark crossbars of the frame work. It was a magnificent castle 5of the renaissance, a chef-d’œuvre of taste mingled with caprice, one of those dwellings where one is impressed by something indescribably ingenious and bold, which from the imagination of the architect seems to pass into one’s own, and take possession of it, raising it above the usages and preoccupations of a positive world. 45 | 46 | I confess that my heart beat fast in giving my name to the lackey commissioned to announce me. I had never seen Madame d’Ionis; she passed for one of the prettiest women in the country, was twenty-two, and had a husband who was neither handsome nor amiable, and who neglected her in order to travel. Her writing was charming, and she found means to show not only a great deal of sense, but still more cleverness in her business letters. Altogether she was a very fine character. This was all that I knew of her, and it was sufficient for me to dread appearing awkward or provincial. I grew pale on entering the salon. My first impression then was one of relief and pleasure, when I found myself in the presence of two stout and very ugly old women, one of whom, Madame the Dowager d’Ionis informed me that her daughter-in-law was at the house of her friends in the neighborhood, and probably would not return before the next day. 47 | 48 | “You are welcome, all the same,” added this matron. “We have a very friendly and grateful feeling for your father, and it appears that we stand in great need of his counsel, which you are without doubt charged to communicate to us.” 49 | 50 | “I came from him,” I replied, “to talk over the affair with Madame d’Ionis.” 51 | """ 52 | testcase1 = text_testcase1 53 | myfield = ALAddendumField(field_name="testcase1") 54 | myfield.overflow_trigger = 160 55 | self.assertLessEqual( 56 | len(myfield.safe_value(overflow_message=" [See addendum]")), 57 | 160, 58 | ) 59 | # print(myfield.safe_value(overflow_message="")) 60 | safe_value = myfield.safe_value(overflow_message="") 61 | self.assertTrue(safe_value.endswith("in the")) 62 | overflow_value = myfield.overflow_value(overflow_message="") 63 | self.assertTrue(overflow_value.startswith("lands")) 64 | self.assertEqual(len(myfield.safe_value(overflow_message="")), 158) 65 | 66 | with_weird_spaces = """Testing here 67 | 68 | 69 | with some very short words, but a whole lot of them, so it'll be over the overflow, over the flow yah know?""" 70 | 71 | field_with_weird_spaces = ALAddendumField( 72 | field_name="with_weird_spaces", overflow_trigger=23 73 | ) 74 | safe_value = field_with_weird_spaces.safe_value(overflow_message="") 75 | self.assertTrue(safe_value.endswith("some"), msg=f"{ safe_value }") 76 | overflow_value = field_with_weird_spaces.overflow_value(overflow_message="") 77 | self.assertTrue(overflow_value.startswith("very"), msg=f"{ overflow_value }") 78 | 79 | 80 | class TestOriginalOrOverflowMessage(unittest.TestCase): 81 | def test_original_value_shorter_than_trigger(self): 82 | testcase2 = "Short text" 83 | test_instance = ALAddendumField(field_name="testcase2") 84 | test_instance.overflow_trigger = 10 85 | 86 | result = test_instance.original_or_overflow_message( 87 | overflow_message="Overflow occurred.", 88 | ) 89 | self.assertEqual( 90 | result, "Short text" 91 | ) # Original value shorter than overflow_trigger 92 | 93 | def test_original_value_exceeds_trigger(self): 94 | testcase3 = "A very long text that exceeds the overflow trigger" 95 | test_instance = ALAddendumField(field_name="testcase3") 96 | test_instance.overflow_trigger = 10 97 | 98 | result = test_instance.original_or_overflow_message( 99 | overflow_message="Overflow occurred.", 100 | preserve_newlines=True, 101 | ) 102 | self.assertEqual( 103 | result, "Overflow occurred." 104 | ) # Original value exceeds the overflow_trigger 105 | 106 | def test_original_value_exceeds_trigger_with_newlines(self): 107 | testcase4 = ( 108 | "A medium\n length text\nthat exceeds\nthe overflow trigger with newlines" 109 | ) 110 | test_instance = ALAddendumField(field_name="testcase4") 111 | test_instance.overflow_trigger = 80 112 | 113 | result = test_instance.original_or_overflow_message( 114 | overflow_message="Overflow occurred.", 115 | preserve_newlines=True, 116 | preserve_words=True, 117 | ) 118 | self.assertEqual( 119 | result, "Overflow occurred." 120 | ) # Original value exceeds the overflow_trigger, but preserve_newlines is True 121 | 122 | 123 | if __name__ == "__main__": 124 | unittest.main() 125 | -------------------------------------------------------------------------------- /docassemble/AssemblyLine/test_language.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | from .language import ( 4 | get_language_list_dropdown, 5 | get_language_list_dropdown_item, 6 | get_tuples, 7 | get_language_list, 8 | get_language_list_item, 9 | ) 10 | import pycountry 11 | import yaml 12 | import os 13 | 14 | 15 | class TestLanguage(unittest.TestCase): 16 | def setUp(self): 17 | self.language_codes = ["en", "fr"] 18 | self.mock_url_action = patch( 19 | "docassemble.AssemblyLine.language.url_action", return_value="mocked_url" 20 | ) 21 | self.mocked_url_action = self.mock_url_action.start() 22 | 23 | # Define relative directory 24 | current_dir = os.path.dirname(os.path.abspath(__file__)) 25 | self.relative_path = os.path.join( 26 | current_dir, "data", "sources", "languages.yml" 27 | ) 28 | 29 | def tearDown(self): 30 | self.mock_url_action.stop() 31 | 32 | def test_get_tuples(self): 33 | result = get_tuples(self.language_codes, languages_path=self.relative_path) 34 | 35 | expected_result = [("English", "en"), ("Français", "fr")] 36 | 37 | self.assertEqual(result, expected_result) 38 | 39 | def test_get_language_list_dropdown(self): 40 | result = get_language_list_dropdown( 41 | self.language_codes, languages_path=self.relative_path 42 | ) 43 | self.assertIn('