├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── release.yml │ └── verify.yml ├── .gitignore ├── CHANGES.txt ├── LICENSE ├── README.rst ├── post-commit.sh ├── pre-commit.sh ├── pyproject.toml ├── pyxform ├── __init__.py ├── aliases.py ├── builder.py ├── constants.py ├── entities │ ├── __init__.py │ ├── entities_parsing.py │ └── entity_declaration.py ├── errors.py ├── external_instance.py ├── file_utils.py ├── instance.py ├── json_form_schema.json ├── parsing │ ├── __init__.py │ ├── expression.py │ ├── instance_expression.py │ └── sheet_headers.py ├── question.py ├── question_type_dictionary.py ├── question_types │ ├── all.xls │ ├── base.xls │ ├── beorse.xls │ └── nigeria.xls ├── section.py ├── survey.py ├── survey_element.py ├── translator.py ├── translators │ └── nigeria.json ├── util │ ├── __init__.py │ └── enum.py ├── utils.py ├── validators │ ├── __init__.py │ ├── enketo_validate │ │ └── __init__.py │ ├── error_cleaner.py │ ├── odk_validate │ │ ├── .last_check │ │ ├── README.rst │ │ ├── __init__.py │ │ └── bin │ │ │ ├── ODK_Validate.jar │ │ │ └── installed.json │ ├── pyxform │ │ ├── __init__.py │ │ ├── android_package_name.py │ │ ├── choices.py │ │ ├── iana_subtags │ │ │ ├── __init__.py │ │ │ ├── iana_subtags_2_characters.txt │ │ │ ├── iana_subtags_3_or_more_characters.txt │ │ │ ├── subtags_updater.py │ │ │ └── validation.py │ │ ├── parameters_generic.py │ │ ├── pyxform_reference.py │ │ ├── question_types.py │ │ ├── select_from_file.py │ │ ├── sheet_misspellings.py │ │ └── translations_checks.py │ ├── updater.py │ ├── util.py │ └── xlsform_spec_test.xml ├── xform2json.py ├── xform_instance_parser.py ├── xls2json.py ├── xls2json_backends.py └── xls2xform.py └── tests ├── __init__.py ├── bug_example_xls ├── ODKValidateWarnings.xlsx ├── UCL_Biomass_Plot_Form.xlsx ├── __init__.py ├── bad_calc.xlsx ├── badly_named_choices_sheet.xls ├── blank_second_row.xls ├── calculate_without_calculation.xls ├── duplicate_columns.xlsx ├── excel_with_macros.xlsm ├── extra_columns.xls ├── group_name_test.xls ├── ict_survey_fails.xls ├── not_closed_group_test.xls ├── spaces_in_choices_header.xls ├── xl_date_ambiguous.xlsx └── xl_date_ambiguous_v1.xlsx ├── example_xls ├── README.rst ├── __init__.py ├── allow_comment_rows_test.xls ├── another_loop.xls ├── attribute_columns_test.xlsx ├── bad_calc.xlsx ├── calculate.xls ├── cascading_select_test_equivalent.xls ├── case_insensitivity.csv ├── case_insensitivity.md ├── case_insensitivity.xls ├── case_insensitivity.xlsx ├── choice_filter_test.xlsx ├── choice_name_as_type.xls ├── choice_name_same_as_select_name.xls ├── default_time_demo.xls ├── extra_columns.xlsx ├── extra_sheet_names.xlsx ├── field-list.xlsx ├── flat_xlsform_test.xlsx ├── fruits.csv ├── gps.csv ├── gps.xls ├── group.csv ├── group.md ├── group.xls ├── group.xlsx ├── group_names_must_be_unique.xls ├── hidden.xls ├── include.csv ├── include.md ├── include.xls ├── include.xlsx ├── include_json.csv ├── include_json.md ├── include_json.xls ├── include_json.xlsx ├── loop.csv ├── loop.md ├── loop.xls ├── loop.xlsx ├── or_other.xlsx ├── pull_data.xlsx ├── repeat_date_test.xls ├── simple_loop.csv ├── simple_loop.xls ├── sms_info.xls ├── spec_test_expected_output.xml ├── specify_other.csv ├── specify_other.md ├── specify_other.xls ├── specify_other.xlsx ├── style_settings.xls ├── survey_no_name.xlsx ├── table-list.xls ├── text_and_integer.csv ├── text_and_integer.md ├── text_and_integer.xls ├── text_and_integer.xlsx ├── tutorial.xls ├── unknown_question_type.xls ├── utf_csv.csv ├── widgets-media │ ├── a.jpg │ ├── b.jpg │ ├── happy.jpg │ ├── img_test.jpg │ └── sad.jpg ├── widgets.csv ├── widgets.xls ├── widgets.xml ├── xlsform_spec_test.xlsx ├── xml_escaping.xls ├── yes_or_no_question.csv ├── yes_or_no_question.md ├── yes_or_no_question.xls └── yes_or_no_question.xlsx ├── fixtures └── strings.ini ├── parsing ├── __init__.py └── test_expression.py ├── pyxform_test_case.py ├── test_allow_mock_accuracy.py ├── test_area.py ├── test_audio_quality.py ├── test_audit.py ├── test_background_audio.py ├── test_background_geopoint.py ├── test_bind_conversions.py ├── test_bug_round_calculation.py ├── test_builder.py ├── test_choices_sheet.py ├── test_dump_and_load.py ├── test_dynamic_default.py ├── test_entities_create.py ├── test_entities_update.py ├── test_expected_output ├── __init__.py ├── attribute_columns_test.xml ├── default_time_demo.xml ├── flat_xlsform_test.xml ├── or_other.xml ├── pull_data.xml ├── repeat_date_test.xml ├── survey_no_name.xml ├── table-list.xml ├── widgets.xml ├── xlsform_spec_test.xml ├── xml_escaping.xml └── yes_or_no_question.json ├── test_external_instances.py ├── test_external_instances_for_selects.py ├── test_fieldlist_labels.py ├── test_fields.py ├── test_file.py ├── test_file_utils.py ├── test_for_loop.py ├── test_form_name.py ├── test_geo.py ├── test_group.py ├── test_groups.py ├── test_guidance_hint.py ├── test_image_app_parameter.py ├── test_j2x_creation.py ├── test_j2x_instantiation.py ├── test_j2x_question.py ├── test_js2x_import_from_json.py ├── test_json2xform.py ├── test_language_warnings.py ├── test_last_saved.py ├── test_levenshtein.py ├── test_loop.py ├── test_metadata.py ├── test_notes.py ├── test_osm.py ├── test_output ├── .test └── __init__.py ├── test_parameters_rows.py ├── test_pyxform_test_case.py ├── test_pyxformtestcase.py ├── test_randomize_itemsets.py ├── test_range.py ├── test_rank.py ├── test_repeat.py ├── test_repeat_template.py ├── test_search_function.py ├── test_secondary_instance_translations.py ├── test_set_geopoint.py ├── test_settings.py ├── test_settings_auto_send_delete.py ├── test_sheet_columns.py ├── test_sms.py ├── test_static_defaults.py ├── test_survey.py ├── test_survey_element.py ├── test_table_list.py ├── test_translations.py ├── test_trigger.py ├── test_tutorial_xls.py ├── test_typed_calculates.py ├── test_unicode_rtl.py ├── test_upload_question.py ├── test_validate_unicode_exception.py ├── test_validator_update.py ├── test_validator_util.py ├── test_validators.py ├── test_warnings.py ├── test_whitespace.py ├── test_xform2json.py ├── test_xls2json.py ├── test_xls2json_backends.py ├── test_xls2json_xls.py ├── test_xls2xform.py ├── test_xlsform_spec.py ├── utils.py ├── validators ├── .last_check ├── .last_check_none ├── __init__.py ├── data │ ├── .small_file │ ├── install_fake.json │ ├── install_fake_old.json │ ├── latest_enketo.json │ ├── latest_odk.json │ ├── linux-dupes.zip │ ├── linux-ideal.zip │ └── linux.zip ├── pyxform │ ├── __init__.py │ ├── test_android_package_name.py │ └── test_pyxform_reference.py └── server.py ├── xform_test_case ├── __init__.py ├── base.py ├── test_bugs.py ├── test_xform_conversion.py └── test_xml.py └── xpath_helpers ├── __init__.py ├── choices.py ├── entities.py ├── questions.py └── settings.py /.gitattributes: -------------------------------------------------------------------------------- 1 | text 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | #### Software and hardware versions 12 | pyxform v1.x.x, Python v 13 | 14 | #### Problem description 15 | 16 | #### Steps to reproduce the problem 17 | 18 | #### Expected behavior 19 | 20 | #### Other information 21 | Things you tried, stack traces, related issues, suggestions on how to fix it... -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Closes # 2 | 3 | #### Why is this the best possible solution? Were any other approaches considered? 4 | 5 | #### What are the regression risks? 6 | 7 | #### Does this change require updates to documentation? If so, please file an issue [here](https://github.com/XLSForm/xlsform.github.io) and include the link below. 8 | 9 | #### Before submitting this PR, please make sure you have: 10 | - [ ] included test cases for core behavior and edge cases in `tests` 11 | - [ ] run `python -m unittest` and verified all tests pass 12 | - [ ] run `ruff format pyxform tests` and `ruff check pyxform tests` to lint code 13 | - [ ] verified that any code or assets from external sources are properly credited in comments -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | python: ['3.10'] 13 | os: [ubuntu-latest] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python }} 20 | 21 | # Install dependencies. 22 | - uses: actions/cache@v4 23 | name: Python cache with dependencies. 24 | id: python-cache 25 | with: 26 | path: ${{ env.pythonLocation }} 27 | key: ${{ env.pythonLocation }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('pyproject.toml') }} 28 | - name: Install dependencies. 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install -e . 32 | pip list 33 | 34 | # Build and publish. 35 | - name: Publish release to PyPI 36 | if: success() 37 | run: | 38 | pip install flit==3.9.0 39 | flit --debug publish --no-use-vcs 40 | env: 41 | FLIT_USERNAME: __token__ 42 | FLIT_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | python: ['3.10'] 11 | os: [ubuntu-latest] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: ${{ matrix.python }} 18 | 19 | # Install dependencies. 20 | - uses: actions/cache@v4 21 | name: Python cache with dependencies. 22 | id: python-cache 23 | with: 24 | path: ${{ env.pythonLocation }} 25 | key: ${{ env.pythonLocation }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('pyproject.toml') }} 26 | - name: Install dependencies. 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -e .[dev] 30 | pip list 31 | 32 | # Linter. 33 | - run: ruff check pyxform tests --no-fix 34 | - run: ruff format pyxform tests --diff 35 | 36 | test: 37 | runs-on: ${{ matrix.os }} 38 | strategy: 39 | # Run all matrix jobs even if one of them fails. 40 | fail-fast: false 41 | matrix: 42 | python: ['3.10', '3.11', '3.12', '3.13'] 43 | os: [ubuntu-latest, macos-latest, windows-latest] 44 | include: 45 | - os: windows-latest 46 | windows_nose_args: --traverse-namespace ./tests 47 | steps: 48 | - uses: actions/checkout@v4 49 | - name: Set up Python 50 | uses: actions/setup-python@v5 51 | with: 52 | python-version: ${{ matrix.python }} 53 | 54 | # Install dependencies. 55 | - uses: actions/cache@v4 56 | name: Python cache with dependencies. 57 | id: python-cache 58 | with: 59 | path: ${{ env.pythonLocation }} 60 | key: ${{ env.pythonLocation }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('pyproject.toml') }} 61 | - name: Install dependencies. 62 | run: | 63 | python -m pip install --upgrade pip 64 | pip install -e .[dev] 65 | pip list 66 | 67 | # Tests. 68 | - name: Run tests 69 | run: python -m unittest --verbose 70 | 71 | # Build and Upload. 72 | - name: Build sdist and wheel. 73 | if: success() 74 | run: | 75 | pip install flit==3.9.0 76 | flit --debug build --no-use-vcs 77 | - name: Upload sdist and wheel. 78 | if: success() 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: pyxform--on-${{ matrix.os }}--py${{ matrix.python }} 82 | path: ${{ github.workspace }}/dist/pyxform* 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | *.swp 4 | *.venv 5 | *.~lock.*# 6 | *.ropeproject 7 | .python-version 8 | .DS_Store 9 | .noseids 10 | tests/test_output 11 | .idea 12 | .vscode 13 | 14 | # folders created by setuptools 15 | build 16 | dist 17 | pyxform.egg-info 18 | pip-wheel-metadata 19 | 20 | # By having build ignored above, this ignores documents created by 21 | # sphinx. Overall, I think this is a good thing. 22 | 23 | # a virtual environment 24 | env 25 | venv 26 | 27 | # ignore pypi manifest 28 | MANIFEST 29 | 30 | # ignore pyxform_validator_update files 31 | pyxform/validators/odk_validate/.last_check 32 | pyxform/validators/odk_validate/latest.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, XLSForm 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /post-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo 3 | if [ -a .commit ] 4 | then 5 | rm .commit 6 | git add -u 7 | git commit --amend -C HEAD --no-verify 8 | fi 9 | exit 10 | -------------------------------------------------------------------------------- /pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # auto check for pep8 so we don't check in bad code 4 | FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -e '\.py$') 5 | 6 | if [ -n "$FILES" ]; then 7 | ruff check pyxform tests 8 | ruff format pyxform tests 9 | fi 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pyxform" 3 | version = "4.0.0" 4 | authors = [ 5 | {name = "github.com/xlsform", email = "support@getodk.org"}, 6 | ] 7 | description = "A Python package to create XForms for ODK Collect." 8 | readme = "README.rst" 9 | requires-python = ">=3.10" 10 | dependencies = [ 11 | "xlrd==2.0.1", # Read XLS files 12 | "openpyxl==3.1.3", # Read XLSX files 13 | "defusedxml==0.7.1", # Parse XML 14 | ] 15 | 16 | [project.optional-dependencies] 17 | # Install with `pip install pyxform[dev]`. 18 | dev = [ 19 | "formencode==2.1.1", # Compare XML 20 | "lxml==5.3.0", # XPath test expressions 21 | "psutil==6.1.0", # Process info for performance tests 22 | "ruff==0.7.1", # Format and lint 23 | ] 24 | 25 | [project.urls] 26 | Homepage = "https://pypi.python.org/pypi/pyxform/" 27 | Repository = "https://github.com/XLSForm/pyxform/" 28 | 29 | [project.scripts] 30 | xls2xform = "pyxform.xls2xform:main_cli" 31 | pyxform_validator_update = "pyxform.validators.updater:main_cli" 32 | 33 | [build-system] 34 | requires = ["flit_core >=3.2,<4"] 35 | build-backend = "flit_core.buildapi" 36 | 37 | [tool.flit.module] 38 | name = "pyxform" 39 | 40 | [tool.flit.sdist] 41 | exclude = ["docs", "tests"] 42 | 43 | [tool.ruff] 44 | line-length = 90 45 | target-version = "py310" 46 | fix = true 47 | show-fixes = true 48 | output-format = "full" 49 | src = ["pyxform", "tests"] 50 | 51 | [tool.ruff.lint] 52 | # By default, ruff enables flake8's F rules, along with a subset of the E rules. 53 | select = [ 54 | "B", # flake8-bugbear 55 | "C4", # flake8-comprehensions 56 | "E", # pycodestyle error 57 | # "ERA", # eradicate (commented out code) 58 | "F", # pyflakes 59 | "I", # isort 60 | "PERF", # perflint 61 | "PIE", # flake8-pie 62 | "PL", # pylint 63 | # "PTH", # flake8-use-pathlib 64 | "PYI", # flake8-pyi 65 | # "RET", # flake8-return 66 | "RUF", # ruff-specific rules 67 | "S", # flake8-bandit 68 | # "SIM", # flake8-simplify 69 | "TRY", # tryceratops 70 | "UP", # pyupgrade 71 | "W", # pycodestyle warning 72 | ] 73 | ignore = [ 74 | "E501", # line-too-long (we have a lot of long strings) 75 | "F821", # undefined-name (doesn't work well with type hints, ruff 0.1.11). 76 | "PERF401", # manual-list-comprehension (false positives on selective transforms) 77 | "PERF402", # manual-list-copy (false positives on selective transforms) 78 | "PLR0911", # too-many-return-statements (complexity not useful to warn every time) 79 | "PLR0912", # too-many-branches (complexity not useful to warn every time) 80 | "PLR0913", # too-many-arguments (complexity not useful to warn every time) 81 | "PLR0915", # too-many-statements (complexity not useful to warn every time) 82 | "PLR2004", # magic-value-comparison (many tests expect certain numbers of things) 83 | "PLW2901", # redefined-loop-name (usually not a bug) 84 | "RUF001", # ambiguous-unicode-character-string (false positives on unicode tests) 85 | "S310", # suspicious-url-open-usage (prone to false positives, ruff 0.1.11) 86 | "S320", # suspicious-xmle-tree-usage (according to defusedxml author lxml is safe) 87 | "S603", # subprocess-without-shell-equals-true (prone to false positives, ruff 0.1.11) 88 | "TRY003", # raise-vanilla-args (reasonable lint but would require large refactor) 89 | ] 90 | # per-file-ignores = {"tests/*" = ["E501"]} 91 | -------------------------------------------------------------------------------- /pyxform/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pyxform is a Python library designed to make authoring XForms for ODK 4 | Collect easy. 5 | """ 6 | 7 | __version__ = "4.0.0" 8 | 9 | from pyxform.builder import ( 10 | SurveyElementBuilder, 11 | create_survey, 12 | create_survey_element_from_dict, 13 | create_survey_from_path, 14 | create_survey_from_xls, 15 | ) 16 | from pyxform.instance import SurveyInstance 17 | from pyxform.question import InputQuestion, MultipleChoiceQuestion, Question 18 | from pyxform.question_type_dictionary import QUESTION_TYPE_DICT 19 | from pyxform.section import Section 20 | from pyxform.survey import Survey 21 | from pyxform.xls2json import SurveyReader as ExcelSurveyReader 22 | 23 | # This is what gets imported when someone imports pyxform 24 | # flake8: noqa 25 | -------------------------------------------------------------------------------- /pyxform/entities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/entities/__init__.py -------------------------------------------------------------------------------- /pyxform/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common base classes for pyxform exceptions. 3 | """ 4 | 5 | 6 | class PyXFormError(Exception): 7 | """Common base class for pyxform exceptions.""" 8 | 9 | 10 | class ValidationError(PyXFormError): 11 | """Common base class for pyxform validation exceptions.""" 12 | 13 | 14 | class PyXFormReadError(PyXFormError): 15 | """Common base class for pyxform exceptions occuring during reading XLSForm data.""" 16 | -------------------------------------------------------------------------------- /pyxform/external_instance.py: -------------------------------------------------------------------------------- 1 | """ 2 | ExternalInstance class module 3 | """ 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from pyxform import constants 8 | from pyxform.survey_element import SURVEY_ELEMENT_FIELDS, SurveyElement 9 | 10 | if TYPE_CHECKING: 11 | from pyxform.survey import Survey 12 | 13 | 14 | EXTERNAL_INSTANCE_EXTRA_FIELDS = (constants.TYPE,) 15 | EXTERNAL_INSTANCE_FIELDS = (*SURVEY_ELEMENT_FIELDS, *EXTERNAL_INSTANCE_EXTRA_FIELDS) 16 | 17 | 18 | class ExternalInstance(SurveyElement): 19 | __slots__ = EXTERNAL_INSTANCE_EXTRA_FIELDS 20 | 21 | @staticmethod 22 | def get_slot_names() -> tuple[str, ...]: 23 | return EXTERNAL_INSTANCE_FIELDS 24 | 25 | def __init__(self, name: str, type: str, **kwargs): 26 | self.type: str = type 27 | super().__init__(name=name, **kwargs) 28 | 29 | def xml_control(self, survey: "Survey"): 30 | """ 31 | No-op since there is no associated form control to place under . 32 | 33 | Exists here because there's a soft abstractmethod in SurveyElement. 34 | """ 35 | -------------------------------------------------------------------------------- /pyxform/file_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | The pyxform file utility functions. 3 | """ 4 | 5 | import glob 6 | import os 7 | 8 | from pyxform import utils 9 | from pyxform.xls2json import SurveyReader 10 | 11 | 12 | def _section_name(path_or_file_name): 13 | directory, filename = os.path.split(path_or_file_name) 14 | section_name, extension = os.path.splitext(filename) 15 | return section_name 16 | 17 | 18 | def load_file_to_dict(path): 19 | """ 20 | Takes a file path and loads it into a nested json dict 21 | following the format in json_form_schema.json 22 | The file may be a xls file or json file. 23 | If it is xls it is converted using xls2json. 24 | """ 25 | if path.endswith(".json"): 26 | name = _section_name(path) 27 | return name, utils.get_pyobj_from_json(path) 28 | else: 29 | name = _section_name(path) 30 | excel_reader = SurveyReader(path, name) 31 | return name, excel_reader.to_json_dict() 32 | 33 | 34 | def collect_compatible_files_in_directory(directory): 35 | """ 36 | create a giant dict out of all the spreadsheets and json forms 37 | in the given directory 38 | """ 39 | available_files = glob.glob(os.path.join(directory, "*.xls")) + glob.glob( 40 | os.path.join(directory, "*.json") 41 | ) 42 | 43 | return dict([load_file_to_dict(f) for f in available_files]) 44 | -------------------------------------------------------------------------------- /pyxform/instance.py: -------------------------------------------------------------------------------- 1 | """ 2 | SurveyInstance class module. 3 | """ 4 | 5 | import os.path 6 | 7 | from pyxform.errors import PyXFormError 8 | from pyxform.xform_instance_parser import parse_xform_instance 9 | 10 | 11 | class SurveyInstance: 12 | def __init__(self, survey_object, **kwargs): 13 | self._survey = survey_object 14 | self.kwargs = kwargs # not sure what might be passed to this 15 | 16 | # does the survey object provide a way to get the key dicts? 17 | self._keys = [c.name for c in self._survey.children] 18 | 19 | self._name = self._survey.name 20 | self._id = self._survey.id_string 21 | 22 | # get xpaths 23 | # - prep for xpaths. 24 | self._survey.xml() 25 | self._xpaths = [x.get_xpath() for x in self._survey._xpath.values()] 26 | 27 | # see "answers(self):" below for explanation of this dict 28 | self._answers = {} 29 | self._orphan_answers = {} 30 | 31 | def keys(self): 32 | return self._keys 33 | 34 | def xpaths(self): 35 | # originally thought of having this method get the xpath stuff 36 | # but survey doesn't like when xml() is called multiple times. 37 | return self._xpaths 38 | 39 | def answer(self, name=None, value=None): 40 | if name is None: 41 | raise PyXFormError("In answering, name must be given") 42 | 43 | # ahh. this is horrible, but we need the xpath dict in survey 44 | # to be up-to-date ...maybe 45 | # self._survey.xml() 46 | 47 | if name in self._survey._xpath.keys(): 48 | self._answers[name] = value 49 | else: 50 | self._orphan_answers[name] = value 51 | 52 | def to_json_dict(self): 53 | children = [] 54 | for k, v in self._answers.items(): 55 | children.append({"node_name": k, "value": v}) 56 | return {"node_name": self._name, "id": self._id, "children": children} 57 | 58 | def to_xml(self): 59 | """ 60 | A horrible way to do this, but it works (until we need the attributes 61 | pumped out in order, etc) 62 | """ 63 | open_str = f"""<{self._name} id="{self._id}">""" 64 | close_str = f"""""" 65 | vals = "" 66 | for k, v in self._answers.items(): 67 | vals += f"<{k}>{v!s}" 68 | 69 | output = open_str + vals + close_str 70 | return output 71 | 72 | def answers(self): 73 | """ 74 | This returns "_answers", which is a dict with the key-value 75 | responses for this given instance. This could be pumped to xml 76 | or returned as a dict for maximum convenience (i.e. testing.) 77 | """ 78 | return self._answers 79 | 80 | def import_from_xml(self, xml_string_or_filename): 81 | if os.path.isfile(xml_string_or_filename): 82 | xml_str = open(xml_string_or_filename, encoding="utf-8").read() 83 | else: 84 | xml_str = xml_string_or_filename 85 | key_val_dict = parse_xform_instance(xml_str) 86 | for k, v in key_val_dict.items(): 87 | self.answer(name=k, value=v) 88 | 89 | def __unicode__(self): 90 | orphan_count = len(self._orphan_answers.keys()) 91 | placed_count = len(self._answers.keys()) 92 | answer_count = orphan_count + placed_count 93 | return "" % ( 94 | answer_count, 95 | placed_count, 96 | orphan_count, 97 | ) 98 | -------------------------------------------------------------------------------- /pyxform/parsing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/parsing/__init__.py -------------------------------------------------------------------------------- /pyxform/question_types/all.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/question_types/all.xls -------------------------------------------------------------------------------- /pyxform/question_types/base.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/question_types/base.xls -------------------------------------------------------------------------------- /pyxform/question_types/beorse.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/question_types/beorse.xls -------------------------------------------------------------------------------- /pyxform/question_types/nigeria.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/question_types/nigeria.xls -------------------------------------------------------------------------------- /pyxform/translator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Translator class module. 3 | """ 4 | 5 | from collections import defaultdict 6 | 7 | 8 | def infinite_dict(): 9 | return defaultdict(infinite_dict) 10 | 11 | 12 | # The big idea with this class structure is I want to do the 13 | # following: 14 | # translator = Translator() 15 | # translator.add_translation(...) 16 | # translator.translate('How are you?').from('English').to('French') 17 | 18 | 19 | class _StringWithLanguageTranslator: 20 | def __init__(self, dictionary): 21 | self._dict = dictionary 22 | 23 | def to_language(self, language): 24 | if language in self._dict: 25 | return self._dict[language] 26 | return None 27 | 28 | 29 | class _StringTranslator: 30 | def __init__(self, dictionary): 31 | self._dict = dictionary 32 | 33 | def from_language(self, language): 34 | dictionary = self._dict[language] 35 | return _StringWithLanguageTranslator(dictionary) 36 | 37 | 38 | class Translator: 39 | def __init__(self): 40 | """ 41 | I'm being super lazy dictionary has to have the form: 42 | {'yes' : {'English' : {'French' : 'oui'}}} 43 | """ 44 | self._dict = infinite_dict() 45 | self._languages = [] 46 | 47 | def add_translation( 48 | self, string, source_language, destination_language, translated_string 49 | ): 50 | for lang in [source_language, destination_language]: 51 | if lang not in self._languages: 52 | self._languages.append(lang) 53 | self._dict[string][source_language][destination_language] = translated_string 54 | 55 | def translate(self, string): 56 | dictionary = self._dict[string] 57 | return _StringTranslator(dictionary) 58 | 59 | def to_json_dict(self): 60 | return self._dict 61 | 62 | 63 | # code used to construct a translator from the excel files from phase II. 64 | # import glob, os 65 | # from xls2json import ExcelReader, print_pyobj_to_json 66 | # from translator import Translator 67 | 68 | # translator = Translator() 69 | 70 | # def add_dict(d): 71 | # keys = d.keys() 72 | # keys.remove(u"English") 73 | # for key in keys: 74 | # yield {u"string" : d[u"English"], 75 | # u"source_language" : u"English", 76 | # u"destination_language" : key, 77 | # u"translated_string" : d[key]} 78 | 79 | # def add_row(d): 80 | # assert type(d)==dict, str(d) 81 | # for k, v in d.items(): 82 | # if type(v)==dict and u"English" in v.keys(): 83 | # for result in add_dict(v): yield result 84 | 85 | # xls_files = glob.glob( os.path.join("translators", "*", "*.xls") ) 86 | # all_translations = [] 87 | # for xls_file in xls_files: 88 | # excel_reader = ExcelReader(xls_file) 89 | # for sheet_name, list_of_dicts in excel_reader.to_json_dict().items(): 90 | # for d in list_of_dicts: 91 | # for result in add_row(d): 92 | # translator.add_translation(**result) 93 | # print_pyobj_to_json(translator.to_json_dict(), "nigeria.json") 94 | -------------------------------------------------------------------------------- /pyxform/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/util/__init__.py -------------------------------------------------------------------------------- /pyxform/util/enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class StrEnum(str, Enum): 5 | """Base Enum class with common helper function.""" 6 | 7 | # Copied from Python 3.11 enum.py. In many cases can use members as strings, but 8 | # sometimes need to deref with ".value" property e.g. `EnumClass.MEMBERNAME.value`. 9 | def __new__(cls, *values): 10 | "values must already be of type `str`" 11 | if len(values) > 3: 12 | raise TypeError(f"too many arguments for str(): {values!r}") 13 | if len(values) == 1: 14 | # it must be a string 15 | if not isinstance(values[0], str): 16 | raise TypeError(f"{values[0]!r} is not a string") 17 | if len(values) >= 2: 18 | # check that encoding argument is a string 19 | if not isinstance(values[1], str): 20 | raise TypeError(f"encoding must be a string, not {values[1]!r}") 21 | if len(values) == 3: 22 | # check that errors argument is a string 23 | if not isinstance(values[2], str): 24 | raise TypeError(f"errors must be a string, not {values[2]!r}") 25 | value = str(*values) 26 | member = str.__new__(cls, value) 27 | member._value_ = value 28 | return member 29 | 30 | @classmethod 31 | def value_list(cls): 32 | return list(cls.__members__.values()) 33 | -------------------------------------------------------------------------------- /pyxform/validators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/validators/__init__.py -------------------------------------------------------------------------------- /pyxform/validators/enketo_validate/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Validate XForms using Enketo validator. 3 | """ 4 | 5 | import os 6 | from typing import TYPE_CHECKING 7 | 8 | from pyxform.validators.error_cleaner import ErrorCleaner 9 | from pyxform.validators.util import ( 10 | XFORM_SPEC_PATH, 11 | check_readable, 12 | decode_stream, 13 | run_popen_with_timeout, 14 | ) 15 | 16 | if TYPE_CHECKING: 17 | from pyxform.validators.util import PopenResult 18 | 19 | 20 | CURRENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) 21 | ENKETO_VALIDATE_PATH = os.path.join(CURRENT_DIRECTORY, "bin", "validate") 22 | 23 | 24 | class EnketoValidateError(Exception): 25 | """Common base class for Enketo validate exceptions.""" 26 | 27 | 28 | def install_exists(): 29 | """ 30 | Check if Enketo-validate is installed. 31 | """ 32 | return os.path.exists(ENKETO_VALIDATE_PATH) 33 | 34 | 35 | def _call_validator(path_to_xform, bin_file_path=ENKETO_VALIDATE_PATH) -> "PopenResult": 36 | return run_popen_with_timeout([bin_file_path, path_to_xform], 100) 37 | 38 | 39 | def install_ok(bin_file_path=ENKETO_VALIDATE_PATH): 40 | """ 41 | Check if Enketo-validate functions as expected. 42 | """ 43 | check_readable(file_path=XFORM_SPEC_PATH) 44 | return_code, _, _, _ = _call_validator( 45 | path_to_xform=XFORM_SPEC_PATH, bin_file_path=bin_file_path 46 | ) 47 | if return_code == 1: 48 | return False 49 | else: 50 | return True 51 | 52 | 53 | def check_xform(path_to_xform): 54 | """ 55 | Check the form with the Enketo validator. 56 | 57 | - return code 1: raise error with the stderr content. 58 | - return code 0: append warning with the stdout content (possibly none). 59 | 60 | :param path_to_xform: Path to the XForm to be validated. 61 | :return: warnings or List[str] 62 | """ 63 | if not install_exists(): 64 | raise OSError( 65 | "Enketo-validate dependency not found. " 66 | "Please use the updater tool to install the latest version." 67 | ) 68 | 69 | returncode, timeout, stdout, stderr = _call_validator(path_to_xform=path_to_xform) 70 | warnings = [] 71 | stderr = decode_stream(stderr) 72 | stdout = decode_stream(stdout) 73 | 74 | if timeout: 75 | return ["XForm took to long to completely validate."] 76 | elif returncode > 0: # Error invalid 77 | raise EnketoValidateError( 78 | "Enketo Validate Errors:\n" + ErrorCleaner.enketo_validate(stderr) 79 | ) 80 | elif returncode == 0: 81 | if stdout: 82 | warnings.append("Enketo Validate Warnings:\n" + stdout) 83 | return warnings 84 | elif returncode < 0: 85 | return ["Bad return code from Enketo Validate."] 86 | -------------------------------------------------------------------------------- /pyxform/validators/error_cleaner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cleans up error messages from the validators. 3 | """ 4 | 5 | import re 6 | 7 | ERROR_MESSAGE_REGEX = re.compile(r"(/[a-z0-9\-_]+(?:/[a-z0-9\-_]+)+)", flags=re.I) 8 | 9 | 10 | class ErrorCleaner: 11 | """Cleans up raw error messages from XForm validators for end users.""" 12 | 13 | @staticmethod 14 | def _replace_xpath_with_tokens(match): 15 | strmatch = match.group() 16 | # eliminate e.g /html/body/select1[@ref=/id_string/elId]/item/value 17 | # instance('q4')/root/item[...] 18 | if strmatch.startswith( 19 | (("/html/body"), ("/root/item"), ("/html/head/model/bind")) 20 | ) or strmatch.endswith("/item/value"): 21 | return strmatch 22 | line = match.group().split("/") 23 | return f"${{{line[len(line) - 1]}}}" 24 | 25 | @staticmethod 26 | def _cleanup_errors(error_message): 27 | error_message = ERROR_MESSAGE_REGEX.sub( 28 | ErrorCleaner._replace_xpath_with_tokens, 29 | error_message, 30 | ) 31 | lines = str(error_message).strip().splitlines() 32 | no_dupes = [ 33 | line for i, line in enumerate(lines) if line != lines[i - 1] or i == 0 34 | ] 35 | return no_dupes 36 | 37 | @staticmethod 38 | def _remove_java_content(line): 39 | # has a java filename (with line number) 40 | has_java_filename = line.find(".java:") != -1 41 | # starts with ' at java class path or method path' 42 | is_a_java_method = line.find("\tat") != -1 43 | if not has_java_filename and not is_a_java_method: 44 | # remove java.lang.RuntimeException 45 | if line.startswith("java.lang.RuntimeException: "): 46 | line = line.replace("java.lang.RuntimeException: ", "") 47 | # remove org.javarosa.xpath.XPathUnhandledException 48 | if line.startswith("org.javarosa.xpath.XPathUnhandledException: "): 49 | line = line.replace("org.javarosa.xpath.XPathUnhandledException: ", "") 50 | # remove java.lang.NullPointerException 51 | if line.startswith("java.lang.NullPointerException"): 52 | line = line.replace("java.lang.NullPointerException", "") 53 | if line.startswith("org.javarosa.xform.parse.XFormParseException"): 54 | line = line.replace("org.javarosa.xform.parse.XFormParseException", "") 55 | return line 56 | 57 | @staticmethod 58 | def _join_final(error_messages): 59 | return "\n".join(line for line in error_messages if line is not None) 60 | 61 | @staticmethod 62 | def odk_validate(error_message): 63 | if "Error: Unable to access jarfile" in error_message: 64 | return error_message # Avoids tokenising the file path. 65 | common = ErrorCleaner._cleanup_errors(error_message) 66 | java_clean = [ErrorCleaner._remove_java_content(i) for i in common] 67 | final_message = ErrorCleaner._join_final(java_clean) 68 | return final_message 69 | 70 | @staticmethod 71 | def enketo_validate(error_message): 72 | common = ErrorCleaner._cleanup_errors(error_message) 73 | final_message = ErrorCleaner._join_final(common) 74 | return final_message 75 | -------------------------------------------------------------------------------- /pyxform/validators/odk_validate/.last_check: -------------------------------------------------------------------------------- 1 | 2024-10-09T16:57:49Z -------------------------------------------------------------------------------- /pyxform/validators/odk_validate/README.rst: -------------------------------------------------------------------------------- 1 | pyxform_validate 2 | ================ 3 | A Python Wrapper for ODK Validate 1.5.0 4 | 5 | How to use: 6 | ----------- 7 | 8 | import odk_validate 9 | 10 | xform_warnings = odk_validate.check_xform("/path/to/xform.xml") 11 | 12 | if len(xform_warnings) == 0: 13 | print "Your XForm is valid with no warnings!" 14 | else: 15 | print "Your XForm is valid but has warnings" 16 | print xform_warnings -------------------------------------------------------------------------------- /pyxform/validators/odk_validate/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | odk_validate.py 3 | A python wrapper around ODK Validate 4 | """ 5 | 6 | import logging 7 | import os 8 | import shutil 9 | import sys 10 | from typing import TYPE_CHECKING 11 | 12 | from pyxform.validators.error_cleaner import ErrorCleaner 13 | from pyxform.validators.util import ( 14 | XFORM_SPEC_PATH, 15 | check_readable, 16 | run_popen_with_timeout, 17 | ) 18 | 19 | if TYPE_CHECKING: 20 | from pyxform.validators.util import PopenResult 21 | 22 | 23 | CURRENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) 24 | ODK_VALIDATE_PATH = os.path.join(CURRENT_DIRECTORY, "bin", "ODK_Validate.jar") 25 | 26 | 27 | class ODKValidateError(Exception): 28 | """ODK Validation exception error.""" 29 | 30 | 31 | def install_exists(): 32 | """Returns True if ODK_VALIDATE_PATH exists.""" 33 | return os.path.exists(ODK_VALIDATE_PATH) 34 | 35 | 36 | def _call_validator(path_to_xform, bin_file_path=ODK_VALIDATE_PATH) -> "PopenResult": 37 | return run_popen_with_timeout( 38 | ["java", "-Djava.awt.headless=true", "-jar", bin_file_path, path_to_xform], 100 39 | ) 40 | 41 | 42 | def install_ok(bin_file_path=ODK_VALIDATE_PATH): 43 | """ 44 | Check if ODK Validate functions as expected. 45 | """ 46 | check_readable(file_path=XFORM_SPEC_PATH) 47 | result = _call_validator( 48 | path_to_xform=XFORM_SPEC_PATH, 49 | bin_file_path=bin_file_path, 50 | ) 51 | if result.return_code == 1: 52 | return False 53 | 54 | return True 55 | 56 | 57 | def check_java_available(): 58 | """ 59 | Check if 'which java' returncode is 0. If not, raise an error since java is required. 60 | """ 61 | java_path = shutil.which(cmd="java") 62 | if java_path is not None: 63 | return 64 | msg = ( 65 | "Form validation failed because Java (8+ required) could not be found. " 66 | "To fix this, please either: 1) install Java, or 2) run pyxform with the " 67 | "--skip_validate flag, or 3) add the installed Java to the environment path." 68 | ) 69 | raise OSError(msg) 70 | 71 | 72 | def check_xform(path_to_xform): 73 | """Run ODK Validate against the XForm in `path_to_xform`. 74 | 75 | Returns an array of warnings if the form is valid. 76 | Throws an exception if it is not 77 | Does not do a LBYL check for compatible java version as per pyxform/#481 78 | """ 79 | # check for available java version 80 | check_java_available() 81 | 82 | # resultcode indicates validity of the form 83 | # timeout indicates whether validation ran out of time to complete 84 | # stdout is not used because it has some warnings that always 85 | # appear and can be ignored. 86 | # stderr is treated as a warning if the form is valid or an error 87 | # if it is invalid. 88 | result = _call_validator(path_to_xform=path_to_xform) 89 | warnings = [] 90 | 91 | if result.timeout: 92 | return ["XForm took to long to completely validate."] 93 | elif result.return_code > 0: # Error invalid 94 | raise ODKValidateError( 95 | "ODK Validate Errors:\n" + ErrorCleaner.odk_validate(result.stderr) 96 | ) 97 | elif result.return_code == 0: 98 | if result.stderr: 99 | warnings.append("ODK Validate Warnings:\n" + result.stderr) 100 | elif result.return_code < 0: 101 | return ["Bad return code from ODK Validate."] 102 | 103 | return warnings 104 | 105 | 106 | if __name__ == "__main__": 107 | logger = logging.getLogger(__name__) 108 | logger.addHandler(logging.StreamHandler()) 109 | logger.setLevel(logging.INFO) 110 | logger.info(__doc__) 111 | 112 | check_xform(sys.argv[1]) 113 | -------------------------------------------------------------------------------- /pyxform/validators/odk_validate/bin/ODK_Validate.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/validators/odk_validate/bin/ODK_Validate.jar -------------------------------------------------------------------------------- /pyxform/validators/odk_validate/bin/installed.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/getodk/validate/releases/179158403", 3 | "assets_url": "https://api.github.com/repos/getodk/validate/releases/179158403/assets", 4 | "upload_url": "https://uploads.github.com/repos/getodk/validate/releases/179158403/assets{?name,label}", 5 | "html_url": "https://github.com/getodk/validate/releases/tag/v1.19.2", 6 | "id": 179158403, 7 | "author": { 8 | "login": "lognaturel", 9 | "id": 967540, 10 | "node_id": "MDQ6VXNlcjk2NzU0MA==", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/967540?v=4", 12 | "gravatar_id": "", 13 | "url": "https://api.github.com/users/lognaturel", 14 | "html_url": "https://github.com/lognaturel", 15 | "followers_url": "https://api.github.com/users/lognaturel/followers", 16 | "following_url": "https://api.github.com/users/lognaturel/following{/other_user}", 17 | "gists_url": "https://api.github.com/users/lognaturel/gists{/gist_id}", 18 | "starred_url": "https://api.github.com/users/lognaturel/starred{/owner}{/repo}", 19 | "subscriptions_url": "https://api.github.com/users/lognaturel/subscriptions", 20 | "organizations_url": "https://api.github.com/users/lognaturel/orgs", 21 | "repos_url": "https://api.github.com/users/lognaturel/repos", 22 | "events_url": "https://api.github.com/users/lognaturel/events{/privacy}", 23 | "received_events_url": "https://api.github.com/users/lognaturel/received_events", 24 | "type": "User", 25 | "site_admin": false 26 | }, 27 | "node_id": "RE_kwDOAmc2ms4Krb2D", 28 | "tag_name": "v1.19.2", 29 | "target_commitish": "master", 30 | "name": "v1.19.2", 31 | "draft": false, 32 | "prerelease": false, 33 | "created_at": "2024-10-09T16:54:24Z", 34 | "published_at": "2024-10-09T16:56:20Z", 35 | "assets": [ 36 | { 37 | "url": "https://api.github.com/repos/getodk/validate/releases/assets/197958252", 38 | "id": 197958252, 39 | "node_id": "RA_kwDOAmc2ms4LzJps", 40 | "name": "ODK-Validate-v1.19.2.jar", 41 | "label": null, 42 | "uploader": { 43 | "login": "lognaturel", 44 | "id": 967540, 45 | "node_id": "MDQ6VXNlcjk2NzU0MA==", 46 | "avatar_url": "https://avatars.githubusercontent.com/u/967540?v=4", 47 | "gravatar_id": "", 48 | "url": "https://api.github.com/users/lognaturel", 49 | "html_url": "https://github.com/lognaturel", 50 | "followers_url": "https://api.github.com/users/lognaturel/followers", 51 | "following_url": "https://api.github.com/users/lognaturel/following{/other_user}", 52 | "gists_url": "https://api.github.com/users/lognaturel/gists{/gist_id}", 53 | "starred_url": "https://api.github.com/users/lognaturel/starred{/owner}{/repo}", 54 | "subscriptions_url": "https://api.github.com/users/lognaturel/subscriptions", 55 | "organizations_url": "https://api.github.com/users/lognaturel/orgs", 56 | "repos_url": "https://api.github.com/users/lognaturel/repos", 57 | "events_url": "https://api.github.com/users/lognaturel/events{/privacy}", 58 | "received_events_url": "https://api.github.com/users/lognaturel/received_events", 59 | "type": "User", 60 | "site_admin": false 61 | }, 62 | "content_type": "application/java-archive", 63 | "state": "uploaded", 64 | "size": 5862216, 65 | "download_count": 0, 66 | "created_at": "2024-10-09T17:03:19Z", 67 | "updated_at": "2024-10-09T17:03:21Z", 68 | "browser_download_url": "https://github.com/getodk/validate/releases/download/v1.19.2/ODK-Validate-v1.19.2.jar" 69 | } 70 | ], 71 | "tarball_url": "https://api.github.com/repos/getodk/validate/tarball/v1.19.2", 72 | "zipball_url": "https://api.github.com/repos/getodk/validate/zipball/v1.19.2", 73 | "body": "## What's Changed\r\n* Add back jackson dependency by @lognaturel in https://github.com/getodk/validate/pull/95\r\n\r\n\r\n**Full Changelog**: https://github.com/getodk/validate/compare/v1.19.1...v1.19.2", 74 | "mentions_count": 1 75 | } 76 | -------------------------------------------------------------------------------- /pyxform/validators/pyxform/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/validators/pyxform/__init__.py -------------------------------------------------------------------------------- /pyxform/validators/pyxform/android_package_name.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | PACKAGE_NAME_REGEX = re.compile(r"[^a-zA-Z0-9._]") 4 | 5 | 6 | def validate_android_package_name(name: str) -> str | None: 7 | prefix = "Parameter 'app' has an invalid Android package name - " 8 | 9 | if not name.strip(): 10 | return f"{prefix}package name is missing." 11 | 12 | if "." not in name: 13 | return f"{prefix}the package name must have at least one '.' separator." 14 | 15 | if name[-1] == ".": 16 | return f"{prefix}the package name cannot end in a '.' separator." 17 | 18 | segments = name.split(".") 19 | if any(segment == "" for segment in segments): 20 | return f"{prefix}package segments must be of non-zero length." 21 | 22 | if any(segment.startswith("_") for segment in segments): 23 | return f"{prefix}the character '_' cannot be the first character in a package name segment." 24 | 25 | if any(segment[0].isdigit() for segment in segments): 26 | return f"{prefix}a digit cannot be the first character in a package name segment." 27 | 28 | for segment in segments: 29 | if PACKAGE_NAME_REGEX.search(segment): 30 | return f"{prefix}the package name can only include letters (a-z, A-Z), numbers (0-9), dots (.), and underscores (_)." 31 | 32 | return None 33 | -------------------------------------------------------------------------------- /pyxform/validators/pyxform/choices.py: -------------------------------------------------------------------------------- 1 | from pyxform import constants 2 | from pyxform.errors import PyXFormError 3 | 4 | INVALID_NAME = ( 5 | "[row : {row}] On the 'choices' sheet, the 'name' value is invalid. " 6 | "Choices must have a name. " 7 | "Learn more: https://xlsform.org/en/#setting-up-your-worksheets" 8 | ) 9 | INVALID_LABEL = ( 10 | "[row : {row}] On the 'choices' sheet, the 'label' value is invalid. " 11 | "Choices should have a label. " 12 | "Learn more: https://xlsform.org/en/#setting-up-your-worksheets" 13 | ) 14 | INVALID_HEADER = ( 15 | "[row : 1] On the 'choices' sheet, the '{column}' value is invalid. " 16 | "Column headers must not be empty and must not contain spaces. " 17 | "Learn more: https://xlsform.org/en/#setting-up-your-worksheets" 18 | ) 19 | INVALID_DUPLICATE = ( 20 | "[row : {row}] On the 'choices' sheet, the 'name' value is invalid. " 21 | "Choice names must be unique for each choice list. " 22 | "If this is intentional, use the setting 'allow_choice_duplicates'. " 23 | "Learn more: https://xlsform.org/#choice-names." 24 | ) 25 | 26 | 27 | def validate_headers( 28 | headers: tuple[tuple[str, ...], ...], warnings: list[str] 29 | ) -> tuple[str, ...]: 30 | def check(): 31 | for header in headers: 32 | header = header[0] 33 | if header != constants.LIST_NAME_S and (" " in header or header == ""): 34 | warnings.append(INVALID_HEADER.format(column=header)) 35 | yield header 36 | 37 | return tuple(check()) 38 | 39 | 40 | def validate_choice_list( 41 | options: list[dict], warnings: list[str], allow_duplicates: bool = False 42 | ) -> None: 43 | seen_options = set() 44 | duplicate_errors = [] 45 | for option in options: 46 | if constants.NAME not in option: 47 | raise PyXFormError(INVALID_NAME.format(row=option["__row"])) 48 | elif constants.LABEL not in option: 49 | warnings.append(INVALID_LABEL.format(row=option["__row"])) 50 | 51 | if not allow_duplicates: 52 | name = option[constants.NAME] 53 | if name in seen_options: 54 | duplicate_errors.append(INVALID_DUPLICATE.format(row=option["__row"])) 55 | else: 56 | seen_options.add(name) 57 | 58 | if duplicate_errors: 59 | raise PyXFormError("\n".join(duplicate_errors)) 60 | 61 | 62 | def validate_and_clean_choices( 63 | choices: dict[str, list[dict]], 64 | warnings: list[str], 65 | headers: tuple[tuple[str, ...], ...], 66 | allow_duplicates: bool = False, 67 | ) -> dict[str, list[dict]]: 68 | """ 69 | Warn about invalid or duplicate choices, and remove choices with invalid headers. 70 | 71 | Choices columns are output as XML elements so they must be valid XML tags. 72 | 73 | :param choices: Choices data from the XLSForm. 74 | :param warnings: Warnings list. 75 | :param headers: choices data headers i.e. unique dict keys. 76 | :param allow_duplicates: If True, duplicate choice names are allowed in the XLSForm. 77 | """ 78 | invalid_headers = validate_headers(headers, warnings) 79 | for options in choices.values(): 80 | validate_choice_list( 81 | options=options, 82 | warnings=warnings, 83 | allow_duplicates=allow_duplicates, 84 | ) 85 | for option in options: 86 | for invalid_header in invalid_headers: 87 | option.pop(invalid_header, None) 88 | option.pop("__row", None) 89 | return choices 90 | -------------------------------------------------------------------------------- /pyxform/validators/pyxform/iana_subtags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/pyxform/validators/pyxform/iana_subtags/__init__.py -------------------------------------------------------------------------------- /pyxform/validators/pyxform/iana_subtags/iana_subtags_2_characters.txt: -------------------------------------------------------------------------------- 1 | aa 2 | ab 3 | ae 4 | af 5 | ak 6 | am 7 | an 8 | ar 9 | as 10 | av 11 | ay 12 | az 13 | ba 14 | be 15 | bg 16 | bh 17 | bi 18 | bm 19 | bn 20 | bo 21 | br 22 | bs 23 | ca 24 | ce 25 | ch 26 | co 27 | cr 28 | cs 29 | cu 30 | cv 31 | cy 32 | da 33 | de 34 | dv 35 | dz 36 | ee 37 | el 38 | en 39 | eo 40 | es 41 | et 42 | eu 43 | fa 44 | ff 45 | fi 46 | fj 47 | fo 48 | fr 49 | fy 50 | ga 51 | gd 52 | gl 53 | gn 54 | gu 55 | gv 56 | ha 57 | he 58 | hi 59 | ho 60 | hr 61 | ht 62 | hu 63 | hy 64 | hz 65 | ia 66 | id 67 | ie 68 | ig 69 | ii 70 | ik 71 | in 72 | io 73 | is 74 | it 75 | iu 76 | iw 77 | ja 78 | ji 79 | jv 80 | jw 81 | ka 82 | kg 83 | ki 84 | kj 85 | kk 86 | kl 87 | km 88 | kn 89 | ko 90 | kr 91 | ks 92 | ku 93 | kv 94 | kw 95 | ky 96 | la 97 | lb 98 | lg 99 | li 100 | ln 101 | lo 102 | lt 103 | lu 104 | lv 105 | mg 106 | mh 107 | mi 108 | mk 109 | ml 110 | mn 111 | mo 112 | mr 113 | ms 114 | mt 115 | my 116 | na 117 | nb 118 | nd 119 | ne 120 | ng 121 | nl 122 | nn 123 | no 124 | nr 125 | nv 126 | ny 127 | oc 128 | oj 129 | om 130 | or 131 | os 132 | pa 133 | pi 134 | pl 135 | ps 136 | pt 137 | qu 138 | rm 139 | rn 140 | ro 141 | ru 142 | rw 143 | sa 144 | sc 145 | sd 146 | se 147 | sg 148 | sh 149 | si 150 | sk 151 | sl 152 | sm 153 | sn 154 | so 155 | sq 156 | sr 157 | ss 158 | st 159 | su 160 | sv 161 | sw 162 | ta 163 | te 164 | tg 165 | th 166 | ti 167 | tk 168 | tl 169 | tn 170 | to 171 | tr 172 | ts 173 | tt 174 | tw 175 | ty 176 | ug 177 | uk 178 | ur 179 | uz 180 | ve 181 | vi 182 | vo 183 | wa 184 | wo 185 | xh 186 | yi 187 | yo 188 | za 189 | zh 190 | zu -------------------------------------------------------------------------------- /pyxform/validators/pyxform/iana_subtags/subtags_updater.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | """ 4 | The IANA tag registry is updated occasionally. Use this script to update pyxform's copy. 5 | 6 | Save (don't commit) a local .txt copy of the full tag registry and run this script. The 7 | registry includes definitions for things other than languages, so the regex looks for only 8 | primary language subtags. The tag registry referenced by the XLSForm docs is: 9 | https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry 10 | 11 | For further reference see the RFC/BCP: https://datatracker.ietf.org/doc/html/rfc5646 12 | """ 13 | 14 | 15 | def update(): 16 | with open("language-subtag-registry.txt", encoding="utf-8") as f1: 17 | matches = re.findall(r"Type: language\nSubtag:\s(.*?)\n", f1.read()) 18 | 19 | with open( 20 | "iana_subtags_2_characters.txt", mode="w", encoding="utf-8", newline="\n" 21 | ) as f2: 22 | f2.write("\n".join(i for i in matches if len(i) == 2)) 23 | 24 | with open( 25 | "iana_subtags_3_or_more_characters.txt", mode="w", encoding="utf-8", newline="\n" 26 | ) as f3: 27 | f3.write("\n".join(i for i in matches if len(i) > 2)) 28 | 29 | 30 | if __name__ == "__main__": 31 | update() 32 | -------------------------------------------------------------------------------- /pyxform/validators/pyxform/iana_subtags/validation.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import lru_cache 3 | from pathlib import Path 4 | 5 | LANG_CODE_REGEX = re.compile(r"\((.*)\)$") 6 | HERE = Path(__file__).parent 7 | 8 | 9 | @lru_cache(maxsize=2) # Expecting to read 2 files. 10 | def read_tags(file_name: str) -> set[str]: 11 | path = HERE / file_name 12 | with open(path, encoding="utf-8") as f: 13 | return {line.strip() for line in f} 14 | 15 | 16 | def get_languages_with_bad_tags(languages): 17 | """ 18 | Returns languages with invalid or missing IANA subtags. 19 | """ 20 | languages_with_bad_tags = [] 21 | for lang in languages: 22 | # Minimum matchable lang code attempt requires 3 characters e.g. "a()". 23 | if lang == "default" or len(lang) < 3: 24 | continue 25 | lang_code = LANG_CODE_REGEX.search(lang) 26 | if not lang_code: 27 | languages_with_bad_tags.append(lang) 28 | else: 29 | lang_match = lang_code.group(1) 30 | # Check the short list first: 190 short codes vs 7948 long codes. 31 | if lang_match in read_tags("iana_subtags_2_characters.txt"): 32 | continue 33 | elif lang_match in read_tags("iana_subtags_3_or_more_characters.txt"): 34 | continue 35 | else: 36 | languages_with_bad_tags.append(lang) 37 | return languages_with_bad_tags 38 | -------------------------------------------------------------------------------- /pyxform/validators/pyxform/parameters_generic.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from typing import Any 3 | 4 | from pyxform.errors import PyXFormError 5 | 6 | PARAMETERS_TYPE = dict[str, Any] 7 | 8 | # Label and value are used to match against user-specified files so case should be preserved. 9 | CASE_SENSITIVE_VALUES = ["label", "value"] 10 | 11 | 12 | def parse(raw_parameters: str) -> PARAMETERS_TYPE: 13 | parts = raw_parameters.split(";") 14 | if len(parts) == 1: 15 | parts = raw_parameters.split(",") 16 | if len(parts) == 1: 17 | parts = raw_parameters.split() 18 | 19 | params = {} 20 | for param in parts: 21 | if "=" not in param: 22 | raise PyXFormError( 23 | "Expecting parameters to be in the form of " 24 | "'parameter1=value parameter2=value'." 25 | ) 26 | k, v = param.split("=")[:2] 27 | key = k.lower().strip() 28 | params[key] = v.strip() if key in CASE_SENSITIVE_VALUES else v.lower().strip() 29 | 30 | return params 31 | 32 | 33 | def validate( 34 | parameters: PARAMETERS_TYPE, 35 | allowed: Sequence[str], 36 | ) -> dict[str, str]: 37 | """ 38 | Raise an error if 'parameters' includes any keys not named in 'allowed'. 39 | """ 40 | extras = set(parameters) - (set(allowed)) 41 | if 0 < len(extras): 42 | msg = ( 43 | "Accepted parameters are '{a}'. " 44 | "The following are invalid parameter(s): '{e}'." 45 | ).format(a=", ".join(sorted(allowed)), e=", ".join(sorted(extras))) 46 | raise PyXFormError(msg) 47 | return parameters 48 | -------------------------------------------------------------------------------- /pyxform/validators/pyxform/pyxform_reference.py: -------------------------------------------------------------------------------- 1 | from pyxform import constants as co 2 | from pyxform.errors import PyXFormError 3 | from pyxform.parsing.expression import parse_expression 4 | 5 | PYXFORM_REFERENCE_INVALID = ( 6 | "[row : {row_number}] On the '{sheet}' sheet, the '{column}' value is invalid. " 7 | "Reference expressions must only include question names, and end with '}}'." 8 | ) 9 | 10 | 11 | def validate_pyxform_reference_syntax( 12 | value: str, sheet_name: str, row_number: int, key: str 13 | ) -> None: 14 | # Needs 3 characters for "${}" plus a name inside, but need to catch ${ for warning. 15 | if not value or len(value) <= 2 or "${" not in value: 16 | return 17 | # Skip columns in potentially large sheets where references are not allowed. 18 | elif sheet_name == co.SURVEY: 19 | if key in {co.TYPE, co.NAME}: 20 | return 21 | elif sheet_name == co.CHOICES: 22 | if key in {co.LIST_NAME_S, co.LIST_NAME_U, co.NAME}: 23 | return 24 | elif sheet_name == co.ENTITIES: 25 | if key in {co.LIST_NAME_S, co.LIST_NAME_U}: 26 | return 27 | 28 | tokens, _ = parse_expression(value) 29 | start_token = None 30 | 31 | for t in tokens: 32 | # The start of an expression. 33 | if t is not None and t.name == "PYXFORM_REF_START" and start_token is None: 34 | start_token = t 35 | # Tokens that are part of an expression. 36 | elif start_token is not None: 37 | if t.name == "NAME": 38 | continue 39 | elif t.name == "PYXFORM_REF_END": 40 | start_token = None 41 | elif t.name in {"PYXFORM_REF_START", "PYXFORM_REF"}: 42 | msg = PYXFORM_REFERENCE_INVALID.format( 43 | sheet=sheet_name, row_number=row_number, column=key 44 | ) 45 | raise PyXFormError(msg) 46 | else: 47 | msg = PYXFORM_REFERENCE_INVALID.format( 48 | sheet=sheet_name, row_number=row_number, column=key 49 | ) 50 | raise PyXFormError(msg) 51 | 52 | if start_token is not None: 53 | msg = PYXFORM_REFERENCE_INVALID.format( 54 | sheet=sheet_name, row_number=row_number, column=key 55 | ) 56 | raise PyXFormError(msg) 57 | -------------------------------------------------------------------------------- /pyxform/validators/pyxform/question_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Validations for question types. 3 | """ 4 | 5 | from pyxform.errors import PyXFormError 6 | from pyxform.parsing.expression import is_pyxform_reference 7 | from pyxform.utils import PYXFORM_REFERENCE_REGEX 8 | 9 | BACKGROUND_GEOPOINT_CALCULATION = "[row : {r}] For 'background-geopoint' questions, the 'calculation' column must be empty." 10 | TRIGGER_INVALID = ( 11 | "[row : {r}] For '{t}' questions, the 'trigger' column must be a reference to another " 12 | "question that exists, in the format ${{question_name_here}}." 13 | ) 14 | 15 | 16 | def validate_background_geopoint_calculation(row: dict, row_num: int) -> bool: 17 | """A background-geopoint must not have a calculation.""" 18 | try: 19 | row["bind"]["calculate"] 20 | except KeyError: 21 | return True 22 | else: 23 | raise PyXFormError(BACKGROUND_GEOPOINT_CALCULATION.format(r=row_num)) 24 | 25 | 26 | def validate_background_geopoint_trigger(row: dict, row_num: int) -> bool: 27 | """A background-geopoint must have a trigger.""" 28 | if not row.get("trigger", False) or not is_pyxform_reference(value=row["trigger"]): 29 | raise PyXFormError(TRIGGER_INVALID.format(r=row_num, t=row["type"])) 30 | return True 31 | 32 | 33 | def validate_references(referrers: list[tuple[dict, int]], questions: set[str]) -> bool: 34 | """Triggers must refer to a question that exists.""" 35 | for row, row_num in referrers: 36 | matches = PYXFORM_REFERENCE_REGEX.match(row["trigger"]) 37 | if matches is not None: 38 | trigger = matches.groups()[0] 39 | if trigger not in questions: 40 | raise PyXFormError(TRIGGER_INVALID.format(r=row_num, t=row["type"])) 41 | return True 42 | -------------------------------------------------------------------------------- /pyxform/validators/pyxform/select_from_file.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | 4 | from pyxform import aliases 5 | from pyxform.constants import EXTERNAL_INSTANCE_EXTENSIONS, ROW_FORMAT_STRING 6 | from pyxform.errors import PyXFormError 7 | 8 | VALUE_OR_LABEL_TEST_REGEX = re.compile(r"^[a-zA-Z_][a-zA-Z0-9\-_\.]*$") 9 | 10 | 11 | def value_or_label_format_msg(name: str, row_number: int) -> str: 12 | return ( 13 | ROW_FORMAT_STRING % str(row_number) 14 | + f" Parameter '{name}' has a value which is not valid." 15 | + " Values must begin with a letter or underscore. Subsequent " 16 | + "characters can include letters, numbers, dashes, underscores, and periods." 17 | ) 18 | 19 | 20 | def value_or_label_test(value: str) -> bool: 21 | query = VALUE_OR_LABEL_TEST_REGEX.search(value) 22 | if query is None: 23 | return False 24 | else: 25 | return query.group(0) == value 26 | 27 | 28 | def value_or_label_check(name: str, value: str, row_number: int) -> None: 29 | """ 30 | Check parameter values for invalid characters for use in a XPath expression. 31 | 32 | For example for a value of "val*", ODK Validate will throw an error like that shown 33 | below. This check looks for characters which seem to avoid the error. 34 | 35 | >> Something broke the parser. See above for a hint. 36 | org.javarosa.xpath.XPathException: XPath evaluation: Parse error in XPath path: [val*]. 37 | Bad node: org.javarosa.xpath.parser.ast.ASTNodeAbstractExpr@63e2203c 38 | 39 | :param name: The name of the parameter value. 40 | :param value: The parameter value to validate. 41 | :param row_number: The survey sheet row number. 42 | """ 43 | if not value_or_label_test(value=value): 44 | msg = value_or_label_format_msg(name=name, row_number=row_number) 45 | raise PyXFormError(msg) 46 | 47 | 48 | def validate_list_name_extension( 49 | select_command: str, list_name: str, row_number: int 50 | ) -> None: 51 | """For select_from_file types, the list_name should end with a supported extension.""" 52 | list_path = Path(list_name) 53 | if select_command in aliases.select_from_file and ( 54 | 1 != len(list_path.suffixes) 55 | or list_path.suffix not in EXTERNAL_INSTANCE_EXTENSIONS 56 | ): 57 | exts = ", ".join(f"'{e}'" for e in EXTERNAL_INSTANCE_EXTENSIONS) 58 | raise PyXFormError( 59 | ROW_FORMAT_STRING % row_number 60 | + f" File name for '{select_command} {list_name}' should end with one of " 61 | + f"the supported file extensions: {exts}" 62 | ) 63 | -------------------------------------------------------------------------------- /pyxform/validators/pyxform/sheet_misspellings.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | 3 | from pyxform import constants 4 | from pyxform.utils import levenshtein_distance 5 | 6 | 7 | def find_sheet_misspellings(key: str, keys: Iterable) -> "str | None": 8 | """ 9 | Find possible sheet name misspellings to warn the user about. 10 | 11 | It's possible that this will warn about sheet names for sheets that have 12 | auxilliary metadata that is not meant for processing by pyxform. For 13 | example the "osm" sheet name may be similar to many other initialisms. 14 | 15 | :param key: The sheet name to look for. 16 | :param keys: The workbook sheet names. 17 | """ 18 | if not keys: 19 | return None 20 | candidates = tuple( 21 | _k # thanks to black 22 | for _k in keys 23 | if 2 >= levenshtein_distance(_k.lower(), key) 24 | and _k not in constants.SUPPORTED_SHEET_NAMES 25 | and not _k.startswith("_") 26 | ) 27 | if 0 < len(candidates): 28 | msg = ( 29 | "When looking for a sheet named '{k}', the following sheets with " 30 | "similar names were found: {c}." 31 | ).format(k=key, c=str(", ".join(f"'{c}'" for c in candidates))) 32 | return msg 33 | else: 34 | return None 35 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/__init__.py -------------------------------------------------------------------------------- /tests/bug_example_xls/ODKValidateWarnings.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/ODKValidateWarnings.xlsx -------------------------------------------------------------------------------- /tests/bug_example_xls/UCL_Biomass_Plot_Form.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/UCL_Biomass_Plot_Form.xlsx -------------------------------------------------------------------------------- /tests/bug_example_xls/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | PATH = os.path.dirname(__file__) 4 | -------------------------------------------------------------------------------- /tests/bug_example_xls/bad_calc.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/bad_calc.xlsx -------------------------------------------------------------------------------- /tests/bug_example_xls/badly_named_choices_sheet.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/badly_named_choices_sheet.xls -------------------------------------------------------------------------------- /tests/bug_example_xls/blank_second_row.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/blank_second_row.xls -------------------------------------------------------------------------------- /tests/bug_example_xls/calculate_without_calculation.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/calculate_without_calculation.xls -------------------------------------------------------------------------------- /tests/bug_example_xls/duplicate_columns.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/duplicate_columns.xlsx -------------------------------------------------------------------------------- /tests/bug_example_xls/excel_with_macros.xlsm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/excel_with_macros.xlsm -------------------------------------------------------------------------------- /tests/bug_example_xls/extra_columns.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/extra_columns.xls -------------------------------------------------------------------------------- /tests/bug_example_xls/group_name_test.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/group_name_test.xls -------------------------------------------------------------------------------- /tests/bug_example_xls/ict_survey_fails.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/ict_survey_fails.xls -------------------------------------------------------------------------------- /tests/bug_example_xls/not_closed_group_test.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/not_closed_group_test.xls -------------------------------------------------------------------------------- /tests/bug_example_xls/spaces_in_choices_header.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/spaces_in_choices_header.xls -------------------------------------------------------------------------------- /tests/bug_example_xls/xl_date_ambiguous.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/xl_date_ambiguous.xlsx -------------------------------------------------------------------------------- /tests/bug_example_xls/xl_date_ambiguous_v1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/bug_example_xls/xl_date_ambiguous_v1.xlsx -------------------------------------------------------------------------------- /tests/example_xls/README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Example Questionnaires 3 | ====================== 4 | 5 | The Microsoft Excel files in this directory demonstrate the xls2xform 6 | syntax. To get a good overview of the syntax look at tutorial.xls. 7 | -------------------------------------------------------------------------------- /tests/example_xls/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | PATH = os.path.dirname(__file__) 4 | -------------------------------------------------------------------------------- /tests/example_xls/allow_comment_rows_test.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/allow_comment_rows_test.xls -------------------------------------------------------------------------------- /tests/example_xls/another_loop.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/another_loop.xls -------------------------------------------------------------------------------- /tests/example_xls/attribute_columns_test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/attribute_columns_test.xlsx -------------------------------------------------------------------------------- /tests/example_xls/bad_calc.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/bad_calc.xlsx -------------------------------------------------------------------------------- /tests/example_xls/calculate.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/calculate.xls -------------------------------------------------------------------------------- /tests/example_xls/cascading_select_test_equivalent.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/cascading_select_test_equivalent.xls -------------------------------------------------------------------------------- /tests/example_xls/case_insensitivity.csv: -------------------------------------------------------------------------------- 1 | SURVEY , 2 | , TYPE , NAME , LABEL::EN , CHOICE_FILTER 3 | , select_one c1 , q1 , Are you good? , 4 | , select_one_external c1 , q2 , Where are you? , YES_NO=${q1} 5 | , osm c1 , q3 , Where exactly? , 6 | CHOICES , 7 | , LIST_NAME , NAME , LABEL::EN 8 | , c1 , n1-c , l1-c 9 | , c1 , n2-c , l2-c 10 | SETTINGS , 11 | , FORM_TITLE , FORM_ID , DEFAULT_LANGUAGE 12 | , Yes or no , YesNo , EN 13 | EXTERNAL_CHOICES , 14 | , LIST_NAME , NAME , LABEL , YES_NO 15 | , c1 , n1-e , l1-e , yes 16 | , c1 , n2-e , l2-e , yes 17 | ENTITIES , 18 | , DATASET , LABEL 19 | , e1 , l1 20 | OSM , 21 | , LIST_NAME , NAME , LABEL 22 | , c1 , n1-o , l1-o 23 | , c1 , n2-o , l2-o 24 | -------------------------------------------------------------------------------- /tests/example_xls/case_insensitivity.md: -------------------------------------------------------------------------------- 1 | | SURVEY | 2 | | | TYPE | NAME | LABEL::EN | CHOICE_FILTER | 3 | | | select_one c1 | q1 | Are you good? | | 4 | | | select_one_external c1 | q2 | Where are you? | YES_NO=${q1} | 5 | | | osm c1 | q3 | Where exactly? | | 6 | | CHOICES | 7 | | | LIST_NAME | NAME | LABEL::EN | 8 | | | c1 | n1-c | l1-c | 9 | | | c1 | n2-c | l2-c | 10 | | SETTINGS | 11 | | | FORM_TITLE | FORM_ID | DEFAULT_LANGUAGE | 12 | | | Yes or no | YesNo | EN | 13 | | EXTERNAL_CHOICES | 14 | | | LIST_NAME | NAME | LABEL | YES_NO | 15 | | | c1 | n1-e | l1-e | yes | 16 | | | c1 | n2-e | l2-e | yes | 17 | | ENTITIES | 18 | | | DATASET | LABEL | 19 | | | e1 | l1 | 20 | | OSM | 21 | | | LIST_NAME | NAME | LABEL | 22 | | | c1 | n1-o | l1-o | 23 | | | c1 | n2-o | l2-o | 24 | -------------------------------------------------------------------------------- /tests/example_xls/case_insensitivity.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/case_insensitivity.xls -------------------------------------------------------------------------------- /tests/example_xls/case_insensitivity.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/case_insensitivity.xlsx -------------------------------------------------------------------------------- /tests/example_xls/choice_filter_test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/choice_filter_test.xlsx -------------------------------------------------------------------------------- /tests/example_xls/choice_name_as_type.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/choice_name_as_type.xls -------------------------------------------------------------------------------- /tests/example_xls/choice_name_same_as_select_name.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/choice_name_same_as_select_name.xls -------------------------------------------------------------------------------- /tests/example_xls/default_time_demo.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/default_time_demo.xls -------------------------------------------------------------------------------- /tests/example_xls/extra_columns.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/extra_columns.xlsx -------------------------------------------------------------------------------- /tests/example_xls/extra_sheet_names.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/extra_sheet_names.xlsx -------------------------------------------------------------------------------- /tests/example_xls/field-list.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/field-list.xlsx -------------------------------------------------------------------------------- /tests/example_xls/flat_xlsform_test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/flat_xlsform_test.xlsx -------------------------------------------------------------------------------- /tests/example_xls/fruits.csv: -------------------------------------------------------------------------------- 1 | name_key,name 2 | mango,Mango 3 | orange,Orange 4 | banana,Banana 5 | papaya,Papaya 6 | -------------------------------------------------------------------------------- /tests/example_xls/gps.csv: -------------------------------------------------------------------------------- 1 | "survey",, 2 | ,"type","name" 3 | ,"gps","location" 4 | "choices",, 5 | ,"list name","value","text:english" 6 | -------------------------------------------------------------------------------- /tests/example_xls/gps.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/gps.xls -------------------------------------------------------------------------------- /tests/example_xls/group.csv: -------------------------------------------------------------------------------- 1 | "survey",,, 2 | ,"type","name","label:English (en)" 3 | ,"text","family_name","What's your family name?" 4 | ,"begin group","father","Father" 5 | ,"phone number","phone_number","What's your father's phone number?" 6 | ,"integer","age","How old is your father?" 7 | ,"end group",, 8 | -------------------------------------------------------------------------------- /tests/example_xls/group.md: -------------------------------------------------------------------------------- 1 | | survey | 2 | | | type | name | label:English (en) | 3 | | | text | family_name | What's your family name? | 4 | | | begin group | father | Father | 5 | | | phone number | phone_number | What's your father's phone number? | 6 | | | integer | age | How old is your father? | 7 | | | end group | | | 8 | -------------------------------------------------------------------------------- /tests/example_xls/group.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/group.xls -------------------------------------------------------------------------------- /tests/example_xls/group.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/group.xlsx -------------------------------------------------------------------------------- /tests/example_xls/group_names_must_be_unique.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/group_names_must_be_unique.xls -------------------------------------------------------------------------------- /tests/example_xls/hidden.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/hidden.xls -------------------------------------------------------------------------------- /tests/example_xls/include.csv: -------------------------------------------------------------------------------- 1 | survey,,, 2 | ,type,name,label:English 3 | ,text,name,What's your name? 4 | ,include,yes_or_no_question,Yes or no question section 5 | choices,,, 6 | ,list name,name,label:english 7 | -------------------------------------------------------------------------------- /tests/example_xls/include.md: -------------------------------------------------------------------------------- 1 | | survey | 2 | | | type | name | label:English | 3 | | | text | name | What's your name? | 4 | | | include | yes_or_no_question | Yes or no question section | 5 | | choices | 6 | | | list name | name | label:english | 7 | -------------------------------------------------------------------------------- /tests/example_xls/include.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/include.xls -------------------------------------------------------------------------------- /tests/example_xls/include.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/include.xlsx -------------------------------------------------------------------------------- /tests/example_xls/include_json.csv: -------------------------------------------------------------------------------- 1 | "survey",,, 2 | ,"type","name","label:English" 3 | ,"include","how_old_are_you", 4 | "choices",,, 5 | ,list name,name,label:english 6 | -------------------------------------------------------------------------------- /tests/example_xls/include_json.md: -------------------------------------------------------------------------------- 1 | | survey | 2 | | | type | name | label:English | 3 | | | include | how_old_are_you | | 4 | | choices | 5 | | | list name | name | label:english | 6 | -------------------------------------------------------------------------------- /tests/example_xls/include_json.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/include_json.xls -------------------------------------------------------------------------------- /tests/example_xls/include_json.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/include_json.xlsx -------------------------------------------------------------------------------- /tests/example_xls/loop.csv: -------------------------------------------------------------------------------- 1 | "survey",,, 2 | ,"type","name","label:english" 3 | ,"select all that apply from toilet_type or specify other","available_toilet_types","What type of toilets are on the premises?" 4 | ,"begin loop over toilet_type","loop_toilet_types", 5 | ,"integer","number","How many %(label)s are on the premises?" 6 | ,"end loop",, 7 | ,,, 8 | "choices",,, 9 | ,"list name","name","label:english" 10 | ,"toilet_type","pit_latrine_with_slab","Pit latrine with slab" 11 | ,"toilet_type","open_pit_latrine","Pit latrine without slab/open pit" 12 | ,"toilet_type","bucket_system","Bucket system" 13 | -------------------------------------------------------------------------------- /tests/example_xls/loop.md: -------------------------------------------------------------------------------- 1 | | survey | 2 | | | type | name | label:english | 3 | | | select all that apply from toilet_type or specify other | available_toilet_types | What type of toilets are on the premises? | 4 | | | begin loop over toilet_type | loop_toilet_types | 5 | | | integer | number | How many %(label)s are on the premises? | 6 | | | end loop | 7 | | choices | 8 | | | list name | name | label:english | 9 | | | toilet_type | pit_latrine_with_slab | Pit latrine with slab | 10 | | | toilet_type | open_pit_latrine | Pit latrine without slab/open pit | 11 | | | toilet_type | bucket_system | Bucket system | 12 | -------------------------------------------------------------------------------- /tests/example_xls/loop.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/loop.xls -------------------------------------------------------------------------------- /tests/example_xls/loop.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/loop.xlsx -------------------------------------------------------------------------------- /tests/example_xls/or_other.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/or_other.xlsx -------------------------------------------------------------------------------- /tests/example_xls/pull_data.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/pull_data.xlsx -------------------------------------------------------------------------------- /tests/example_xls/repeat_date_test.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/repeat_date_test.xls -------------------------------------------------------------------------------- /tests/example_xls/simple_loop.csv: -------------------------------------------------------------------------------- 1 | "survey",,, 2 | ,"type","name","label:English" 3 | ,"begin loop over my_columns","my_table","My Table" 4 | ,"integer","count","How many are there in this group?" 5 | ,"end loop",, 6 | ,,, 7 | "choices",,, 8 | ,"list name","name","label:English" 9 | ,"my_columns","col1","Column 1" 10 | ,"my_columns","col2","Column 2" 11 | -------------------------------------------------------------------------------- /tests/example_xls/simple_loop.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/simple_loop.xls -------------------------------------------------------------------------------- /tests/example_xls/sms_info.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/sms_info.xls -------------------------------------------------------------------------------- /tests/example_xls/specify_other.csv: -------------------------------------------------------------------------------- 1 | "survey",,, 2 | ,"type","name","label:English" 3 | ,"select one from sexes or specify other","sex","What sex are you?" 4 | ,,, 5 | "choices",,, 6 | ,"list name","name","label:English" 7 | ,"sexes","male","Male" 8 | ,"sexes","female","Female" 9 | -------------------------------------------------------------------------------- /tests/example_xls/specify_other.md: -------------------------------------------------------------------------------- 1 | | survey | 2 | | | type | name | label:English | 3 | | | select one from sexes or specify other | sex | What sex are you? | 4 | | choices | 5 | | | list name | name | label:English | 6 | | | sexes | male | Male | 7 | | | sexes | female | Female | 8 | -------------------------------------------------------------------------------- /tests/example_xls/specify_other.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/specify_other.xls -------------------------------------------------------------------------------- /tests/example_xls/specify_other.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/specify_other.xlsx -------------------------------------------------------------------------------- /tests/example_xls/style_settings.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/style_settings.xls -------------------------------------------------------------------------------- /tests/example_xls/survey_no_name.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/survey_no_name.xlsx -------------------------------------------------------------------------------- /tests/example_xls/table-list.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/table-list.xls -------------------------------------------------------------------------------- /tests/example_xls/text_and_integer.csv: -------------------------------------------------------------------------------- 1 | survey,,, 2 | ,type,name,label:english 3 | ,text,your_name,What is your name? 4 | ,integer,your_age,How many years old are you? 5 | choices,,, 6 | ,list name,name,label:english 7 | -------------------------------------------------------------------------------- /tests/example_xls/text_and_integer.md: -------------------------------------------------------------------------------- 1 | | survey | 2 | | | type | name | label:english | 3 | | | text | your_name | What is your name? | 4 | | | integer | your_age | How many years old are you? | 5 | | choices | 6 | | | list name | name | label:english | 7 | -------------------------------------------------------------------------------- /tests/example_xls/text_and_integer.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/text_and_integer.xls -------------------------------------------------------------------------------- /tests/example_xls/text_and_integer.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/text_and_integer.xlsx -------------------------------------------------------------------------------- /tests/example_xls/tutorial.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/tutorial.xls -------------------------------------------------------------------------------- /tests/example_xls/unknown_question_type.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/unknown_question_type.xls -------------------------------------------------------------------------------- /tests/example_xls/utf_csv.csv: -------------------------------------------------------------------------------- 1 | "survey",,, 2 | ,"name","type","label" 3 | ,"burger_toppings","text","…what toppings do you prefer on your 🍔s?" -------------------------------------------------------------------------------- /tests/example_xls/widgets-media/a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/widgets-media/a.jpg -------------------------------------------------------------------------------- /tests/example_xls/widgets-media/b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/widgets-media/b.jpg -------------------------------------------------------------------------------- /tests/example_xls/widgets-media/happy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/widgets-media/happy.jpg -------------------------------------------------------------------------------- /tests/example_xls/widgets-media/img_test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/widgets-media/img_test.jpg -------------------------------------------------------------------------------- /tests/example_xls/widgets-media/sad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/widgets-media/sad.jpg -------------------------------------------------------------------------------- /tests/example_xls/widgets.csv: -------------------------------------------------------------------------------- 1 | "survey",,,,,,,,,, 2 | ,"type","name","label","hint","bind:constraint","bind:jr:constraintMsg","default","bind:readonly","bind:relevant","control:appearance" 3 | ,"string","my_string","string widget","can be short or very long",,,,,, 4 | ,"int","my_int","integer widget","try entering a number < 10",". < 10","number must be less than 10",,,, 5 | ,"decimal","my_decimal","decimal widget","only numbers > 10.51 and < 18.39",". > 10.51 and . < 18.39","number must be between 10.51 and 18.39",18.31,,, 6 | ,"date","my_date","date widget","only future dates allowed",". >= today()","only future dates allowed","2010-06-15",,, 7 | ,"time","my_time","time widget","testing time",,,,,, 8 | ,"select all that apply from list","my_select","select multiple widget","don't pick c and d together","not(selected(., 'c') and selected(., 'd'))","option c and d cannot be selected together","a c",,, 9 | ,"select one from list2","my_select1","select one widget","scroll down to see default selection",,,8,,, 10 | ,"trigger","my_trigger","acknowledge widget","need to push button",,,,,, 11 | ,"string","my_output","review widget. is your email still ${my_trigger}?","long hint: there is an upcoming section.",,,,"true()",, 12 | ,"geopoint","my_geopoint","geopoint widget","this will get gps location",,,,,, 13 | ,"barcode","my_barcode","barcode widget","scans multi-format 1d/2d barcodes",,,,,, 14 | ,"image","my_image","image widget","this will launch the camera",,,,,, 15 | ,"audio","my_audio","audio widget","this will launch the audio recorder",,,,,, 16 | ,"video","my_video","video widget","this will launch the video recorder",,,,,, 17 | ,"string","numberAsString","String field that uses only numbers (plus a couple extra)","Takes 0-9, -, +, ., space, and comma",,,,,,"numbers" 18 | ,"geopoint","locationMap","Geopoint with map Widget","Note: this uses DATA and requires a connection",,,,,,"maps" 19 | ,"dateTime","dateTime","Date and Time Widget",,,,,,, 20 | ,"select one from list","spinner","Spinner Widget: Select 1",,,,,,,"minimal" 21 | ,"select all that apply from list","spinner_all","Spinner Widget: Select All",,,,,,,"minimal" 22 | ,"select one from list","selectadvance","Select Widget - Auto Advance",,,,,,,"quick" 23 | ,"select one from list","autocomplete","Select Widget - Auto Complete",,,,,,,"autocomplete" 24 | "choices",,,,,,,,,, 25 | ,"list name","name","label",,,,,,, 26 | ,"list","a","option a",,,,,,, 27 | ,"list","b","option b",,,,,,, 28 | ,"list","c","option c",,,,,,, 29 | ,"list","d","option d",,,,,,, 30 | ,"list2",1,"option 1",,,,,,, 31 | ,"list2",2,"option 2",,,,,,, 32 | ,"list2",3,"option 3",,,,,,, 33 | ,"list2",4,"option 4",,,,,,, 34 | ,"list2",5,"option 5",,,,,,, 35 | ,"list2",6,"option 6",,,,,,, 36 | ,"list2",7,"option 7",,,,,,, 37 | ,"list2",8,"option 8",,,,,,, 38 | "settings",,,,,,,,,, 39 | ,"add_none_option",,,,,,,,, 40 | ,FALSE,,,,,,,,, 41 | -------------------------------------------------------------------------------- /tests/example_xls/widgets.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/widgets.xls -------------------------------------------------------------------------------- /tests/example_xls/xlsform_spec_test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/xlsform_spec_test.xlsx -------------------------------------------------------------------------------- /tests/example_xls/xml_escaping.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/xml_escaping.xls -------------------------------------------------------------------------------- /tests/example_xls/yes_or_no_question.csv: -------------------------------------------------------------------------------- 1 | "survey",,, 2 | ,"type","name","label:english" 3 | ,"select one from yes_or_no","good_day","have you had a good day today?" 4 | ,,, 5 | "choices",,, 6 | ,"list name","name","label:english" 7 | ,"yes_or_no","yes","yes" 8 | ,"yes_or_no","no","no" 9 | -------------------------------------------------------------------------------- /tests/example_xls/yes_or_no_question.md: -------------------------------------------------------------------------------- 1 | | survey | 2 | | | type | name | label:english | 3 | | | select one from yes_or_no | good_day | have you had a good day today? | 4 | | choices | 5 | | | list name | name | label:english | 6 | | | yes_or_no | yes | yes | 7 | | | yes_or_no | no | no | 8 | -------------------------------------------------------------------------------- /tests/example_xls/yes_or_no_question.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/yes_or_no_question.xls -------------------------------------------------------------------------------- /tests/example_xls/yes_or_no_question.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/example_xls/yes_or_no_question.xlsx -------------------------------------------------------------------------------- /tests/parsing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XLSForm/pyxform/5b36cca51fa6f917ed4b4727da0b4d67e8d6d35b/tests/parsing/__init__.py -------------------------------------------------------------------------------- /tests/parsing/test_expression.py: -------------------------------------------------------------------------------- 1 | from pyxform.parsing.expression import is_xml_tag 2 | 3 | from tests.pyxform_test_case import PyxformTestCase 4 | 5 | positive = [ 6 | ("A", "Single uppercase letter"), 7 | ("ab", "Lowercase letters"), 8 | ("_u", "Leading underscore"), 9 | ("A12", "Leading uppercase letter with digit"), 10 | ("A-1.23", "Leading uppercase letter with hyphen, period, and digit"), 11 | ("Name123-456", "Mixed case, digits, hyphen"), 12 | ("𐐀n", "Leading unicode"), 13 | ("Αλ", "Following unicode"), 14 | ("name:name", "NCName, colon, NCName"), 15 | ("name_with_colon:_and_extras", "NCName, colon, NCName (non-letter characters)"), 16 | # Other special character tokens are excluded by ncname_regex. 17 | ("nameor", "Contains another parser token (or)"), 18 | ("nameand", "Contains another parser token (and)"), 19 | ("namemod", "Contains another parser token (mod)"), 20 | ("namediv", "Contains another parser token (div)"), 21 | ] 22 | 23 | negative = [ 24 | ("", "Empty string"), 25 | (" ", "Space"), 26 | ("123name", "Leading digit"), 27 | ("-name", "Leading hyphen"), 28 | (".name", "Leading period"), 29 | (":name", "Leading colon"), 30 | ("name$", "Invalid character ($)"), 31 | ("name with space", "Invalid character (space)"), 32 | ("na@me", "Invalid character (@)"), 33 | ("na#me", "Invalid character (#)"), 34 | ("name:.name", "Invalid character (in local name)"), 35 | ("-name:name", "Invalid character (in namespace)"), 36 | ("$name:@name", "Invalid character (in both names)"), 37 | ("name:name:name", "Invalid structure (multiple colons)"), 38 | ] 39 | 40 | 41 | class TestExpression(PyxformTestCase): 42 | def test_is_xml_tag__positive(self): 43 | """Should accept positive match cases i.e. valid xml tag names.""" 44 | for case, description in positive: 45 | with self.subTest(case=case, description=description): 46 | self.assertTrue(is_xml_tag(case)) 47 | 48 | def test_is_xml_tag__negative(self): 49 | """Should reject negative match cases i.e. invalid xml tag names.""" 50 | for case, description in negative: 51 | with self.subTest(case=case, description=description): 52 | self.assertFalse(is_xml_tag(case)) 53 | -------------------------------------------------------------------------------- /tests/test_area.py: -------------------------------------------------------------------------------- 1 | """ 2 | AreaTest - test enclosed-area(geo_shape) calculation. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class AreaTest(PyxformTestCase): 9 | """ 10 | AreaTest - test enclosed-area(geo_shape) calculation. 11 | """ 12 | 13 | def test_area(self): 14 | d = ( 15 | "38.253094215699576 21.756382658677467;38.25021274773806 21.756382658677467;" 16 | "38.25007793942195 21.763892843919166;38.25290886154963 21.763935759263404;" 17 | "38.25146813817506 21.758421137528785" 18 | ) 19 | self.assertPyxformXform( 20 | md=f""" 21 | | survey | | | | | | 22 | | | type | name | label | calculation | default | 23 | | | geoshape | geoshape1 | Draw shape... | | {d} | 24 | | | calculate | result | | enclosed-area(${{geoshape1}}) | | 25 | """, 26 | xml__xpath_match=[ 27 | "/h:html/h:head/x:model/x:bind[@calculate='enclosed-area( /test_name/geoshape1 )' " 28 | + " and @nodeset='/test_name/result' and @type='string']", 29 | "/h:html/h:head/x:model/x:instance/x:test_name[x:geoshape1]", 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /tests/test_audio_quality.py: -------------------------------------------------------------------------------- 1 | from tests.pyxform_test_case import PyxformTestCase 2 | 3 | 4 | class AudioQualityTest(PyxformTestCase): 5 | def test_voice_only(self): 6 | self.assertPyxformXform( 7 | name="data", 8 | md=""" 9 | | survey | | | | | 10 | | | type | name | label | parameters | 11 | | | audio | audio | Audio | quality=voice-only | 12 | """, 13 | xml__contains=[ 14 | 'xmlns:odk="http://www.opendatakit.org/xforms"', 15 | '', 16 | ], 17 | ) 18 | 19 | def test_low(self): 20 | self.assertPyxformXform( 21 | name="data", 22 | md=""" 23 | | survey | | | | | 24 | | | type | name | label | parameters | 25 | | | audio | audio | Audio | quality=low | 26 | """, 27 | xml__contains=[ 28 | 'xmlns:odk="http://www.opendatakit.org/xforms"', 29 | '', 30 | ], 31 | ) 32 | 33 | def test_normal(self): 34 | self.assertPyxformXform( 35 | name="data", 36 | md=""" 37 | | survey | | | | | 38 | | | type | name | label | parameters | 39 | | | audio | audio | Audio | quality=normal | 40 | """, 41 | xml__contains=[ 42 | 'xmlns:odk="http://www.opendatakit.org/xforms"', 43 | '', 44 | ], 45 | ) 46 | 47 | def test_external(self): 48 | self.assertPyxformXform( 49 | name="data", 50 | md=""" 51 | | survey | | | | | 52 | | | type | name | label | parameters | 53 | | | audio | audio | Audio | quality=external | 54 | """, 55 | xml__contains=[ 56 | 'xmlns:odk="http://www.opendatakit.org/xforms"', 57 | '', 58 | ], 59 | ) 60 | 61 | def test_foo_fails(self): 62 | self.assertPyxformXform( 63 | name="data", 64 | md=""" 65 | | survey | | | | | 66 | | | type | name | label | parameters | 67 | | | audio | audio | Audio | quality=foo | 68 | """, 69 | errored=True, 70 | error__contains=["Invalid value for quality."], 71 | ) 72 | -------------------------------------------------------------------------------- /tests/test_background_audio.py: -------------------------------------------------------------------------------- 1 | from tests.pyxform_test_case import PyxformTestCase 2 | 3 | 4 | class BackgroundAudioTest(PyxformTestCase): 5 | def test_background_audio(self): 6 | self.assertPyxformXform( 7 | name="data", 8 | md=""" 9 | | survey | | | 10 | | | type | name | 11 | | | background-audio | my_recording | 12 | """, 13 | xml__contains=[ 14 | '', 15 | '', 16 | ], 17 | ) 18 | 19 | def test_background_audio_voice_only(self): 20 | self.assertPyxformXform( 21 | name="data", 22 | md=""" 23 | | survey | | | | 24 | | | type | name | parameters | 25 | | | background-audio | my_recording | quality=voice-only | 26 | """, 27 | xml__contains=[ 28 | '', 29 | ], 30 | ) 31 | 32 | def test_background_audio_low(self): 33 | self.assertPyxformXform( 34 | name="data", 35 | md=""" 36 | | survey | | | | 37 | | | type | name | parameters | 38 | | | background-audio | my_recording | quality=low | 39 | """, 40 | xml__contains=[ 41 | '', 42 | ], 43 | ) 44 | 45 | def test_background_audio_normal(self): 46 | self.assertPyxformXform( 47 | name="data", 48 | md=""" 49 | | survey | | | | 50 | | | type | name | parameters | 51 | | | background-audio | my_recording | quality=normal | 52 | """, 53 | xml__contains=[ 54 | '', 55 | ], 56 | ) 57 | 58 | def test_external_quality_fails(self): 59 | self.assertPyxformXform( 60 | name="data", 61 | md=""" 62 | | survey | | | | 63 | | | type | name | parameters | 64 | | | background-audio | my_recording | quality=external | 65 | """, 66 | errored=True, 67 | error__contains=["Invalid value for quality."], 68 | ) 69 | 70 | def test_foo_quality_fails(self): 71 | self.assertPyxformXform( 72 | name="data", 73 | md=""" 74 | | survey | | | | 75 | | | type | name | parameters | 76 | | | background-audio | my_recording | quality=foo | 77 | """, 78 | errored=True, 79 | error__contains=["Invalid value for quality."], 80 | ) 81 | -------------------------------------------------------------------------------- /tests/test_bind_conversions.py: -------------------------------------------------------------------------------- 1 | """ 2 | BindConversionsTest - test bind conversions. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class BindConversionsTest(PyxformTestCase): 9 | """ 10 | BindConversionsTest - test bind conversions 11 | """ 12 | 13 | def test_bind_readonly_conversion(self): 14 | self.assertPyxformXform( 15 | name="data", 16 | md=""" 17 | | survey | | | | | 18 | | | type | name | label | readonly | 19 | | | text | text | text | yes | 20 | """, 21 | xml__contains=[' required, ', 47 | ], 48 | ) 49 | 50 | def test_bind_constraint_conversion(self): 51 | self.assertPyxformXform( 52 | name="data", 53 | md=""" 54 | | survey | | | | | 55 | | | type | name | label | constraint_message | 56 | | | text | text | text | yes | 57 | """, 58 | xml__contains=[ 59 | ' 1 | too short ${foo} | 71 | """, 72 | xml__contains=[ 73 | ' too short ', 75 | ], 76 | ) 77 | 78 | def test_bind_custom_conversion(self): 79 | self.assertPyxformXform( 80 | name="data", 81 | md=""" 82 | | survey | | | | | 83 | | | type | name | label | bind::foo | 84 | | | text | text | text | bar | 85 | """, 86 | xml__contains=['"""], 19 | ) 20 | -------------------------------------------------------------------------------- /tests/test_dump_and_load.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test multiple XLSForm can be generated successfully. 3 | """ 4 | 5 | import os 6 | from pathlib import Path 7 | from unittest import TestCase 8 | 9 | from pyxform.builder import create_survey_from_path 10 | 11 | from tests import utils 12 | 13 | 14 | class DumpAndLoadTests(TestCase): 15 | def setUp(self): 16 | self.excel_files = [ 17 | "gps.xls", 18 | # "include.xls", 19 | "specify_other.xls", 20 | "group.xls", 21 | "loop.xls", 22 | "text_and_integer.xls", 23 | # todo: this is looking for json that is created (and 24 | # deleted) by another test, is should just add that json 25 | # to the directory. 26 | # "include_json.xls", 27 | "simple_loop.xls", 28 | "yes_or_no_question.xls", 29 | ] 30 | self.surveys = {} 31 | self.this_directory = os.path.dirname(__file__) 32 | for filename in self.excel_files: 33 | path = utils.path_to_text_fixture(filename) 34 | self.surveys[filename] = create_survey_from_path(path) 35 | 36 | def test_load_from_dump(self): 37 | for survey in self.surveys.values(): 38 | survey.json_dump() 39 | path = survey.name + ".json" 40 | survey_from_dump = create_survey_from_path(path) 41 | self.assertEqual(survey.to_json_dict(), survey_from_dump.to_json_dict()) 42 | 43 | def tearDown(self): 44 | for survey in self.surveys.values(): 45 | path = Path(survey.name + ".json") 46 | path.unlink(missing_ok=True) 47 | -------------------------------------------------------------------------------- /tests/test_expected_output/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | PATH = os.path.dirname(__file__) 4 | -------------------------------------------------------------------------------- /tests/test_expected_output/default_time_demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Default TIme Demo 5 | 6 | 7 | 8 | 09:30:00 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/test_expected_output/or_other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | or_other 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | red 21 | no 22 | 23 | 24 | 25 | green 26 | no 27 | 28 | 29 | 30 | blue 31 | no 32 | 33 | 34 | 35 | mauve 36 | yes 37 | 38 | 39 | 40 | apricot 41 | yes 42 | 43 | 44 | 45 | other 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /tests/test_expected_output/pull_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | pull_data 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/test_expected_output/survey_no_name.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | survey_no_name 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/test_expected_output/xml_escaping.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | xml_escaping 5 | 6 | 7 | 8 | 9 | jr://images/a.jpg 10 | 11 | 12 | jr://images/b.jpg 13 | 14 | 15 | jr://images/happy.jpg 16 | 17 | 18 | jr://images/sad.jpg 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | a 36 | 37 | 38 | 39 | b 40 | 41 | 42 | 43 | c 44 | 45 | 46 | 47 | d 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 1 56 | 57 | 58 | 59 | 2 60 | 61 | 62 | 63 | 3 64 | 65 | 66 | 67 | 4 68 | 69 | 70 | 71 | 5 72 | 73 | 74 | 75 | 6 76 | 77 | 78 | 79 | 7 80 | 81 | 82 | 83 | 8 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | yes 92 | 93 | 94 | 95 | no 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | a_b-0 104 | a 105 | 106 | 107 | a_b-1 108 | b 109 | 110 | 111 | 112 | 113 | 114 | 115 | happy_sad-0 116 | happy 117 | 118 | 119 | happy_sad-1 120 | sad 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 134 | 135 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /tests/test_expected_output/yes_or_no_question.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yes_or_no_question", 3 | "title": "yes_or_no_question", 4 | "sms_keyword": "yes_or_no_question", 5 | "default_language": "default", 6 | "id_string": "yes_or_no_question", 7 | "type": "survey", 8 | "children": [ 9 | { 10 | "choices": [ 11 | { 12 | "name": "yes", 13 | "label": { 14 | "english": "yes" 15 | } 16 | }, 17 | { 18 | "name": "no", 19 | "label": { 20 | "english": "no" 21 | } 22 | } 23 | ], 24 | "type": "select one", 25 | "name": "good_day", 26 | "itemset": "yes_or_no", 27 | "list_name": "yes_or_no", 28 | "parameters": {}, 29 | "label": { 30 | "english": "have you had a good day today?" 31 | } 32 | }, 33 | { 34 | "control": { 35 | "bodyless": true 36 | }, 37 | "type": "group", 38 | "name": "meta", 39 | "children": [ 40 | { 41 | "bind": { 42 | "readonly": "true()", 43 | "jr:preload": "uid" 44 | }, 45 | "type": "calculate", 46 | "name": "instanceID" 47 | } 48 | ] 49 | } 50 | ], 51 | "choices": { 52 | "yes_or_no": [ 53 | { 54 | "label": { 55 | "english": "yes" 56 | }, 57 | "name": "yes" 58 | }, 59 | { 60 | "label": { 61 | "english": "no" 62 | }, 63 | "name": "no" 64 | } 65 | ] 66 | } 67 | } -------------------------------------------------------------------------------- /tests/test_fieldlist_labels.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test field-list labels 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class FieldListLabels(PyxformTestCase): 9 | """Test unlabeled group""" 10 | 11 | def test_unlabeled_group(self): 12 | self.assertPyxformXform( 13 | md=""" 14 | | survey | | | | 15 | | | type | name | label | 16 | | | begin_group | my-group | | 17 | | | text | my-text | my-text | 18 | | | end_group | | | 19 | """, 20 | warnings_count=1, 21 | warnings__contains=["[row : 2] Group has no label"], 22 | ) 23 | 24 | def test_unlabeled_group_alternate_syntax(self): 25 | self.assertPyxformXform( 26 | md=""" 27 | | survey | | | | 28 | | | type | name | label::English (en) | 29 | | | begin group | my-group | | 30 | | | text | my-text | my-text | 31 | | | end group | | | 32 | """, 33 | warnings_count=1, 34 | warnings__contains=["[row : 2] Group has no label"], 35 | ) 36 | 37 | def test_unlabeled_group_fieldlist(self): 38 | self.assertPyxformXform( 39 | md=""" 40 | | survey | | | | | 41 | | | type | name | label | appearance | 42 | | | begin_group | my-group | | field-list | 43 | | | text | my-text | my-text | | 44 | | | end_group | | | | 45 | """, 46 | warnings_count=0, 47 | xml__xpath_match=[ 48 | """ 49 | /h:html/h:body/x:group[ 50 | @ref = '/test_name/my-group' and @appearance='field-list' 51 | ] 52 | """ 53 | ], 54 | ) 55 | 56 | def test_unlabeled_group_fieldlist_alternate_syntax(self): 57 | self.assertPyxformXform( 58 | md=""" 59 | | survey | | | | | 60 | | | type | name | label | appearance | 61 | | | begin group | my-group | | field-list | 62 | | | text | my-text | my-text | | 63 | | | end group | | | | 64 | """, 65 | warnings_count=0, 66 | ) 67 | 68 | def test_unlabeled_repeat(self): 69 | self.assertPyxformXform( 70 | md=""" 71 | | survey | | | | 72 | | | type | name | label | 73 | | | begin_repeat | my-repeat | | 74 | | | text | my-text | my-text | 75 | | | end_repeat | | | 76 | """, 77 | warnings_count=1, 78 | warnings__contains=["[row : 2] Repeat has no label"], 79 | ) 80 | 81 | def test_unlabeled_repeat_fieldlist(self): 82 | self.assertPyxformXform( 83 | md=""" 84 | | survey | | | | | 85 | | | type | name | label | appearance | 86 | | | begin_repeat | my-repeat | | field-list | 87 | | | text | my-text | my-text | | 88 | | | end_repeat | | | | 89 | """, 90 | warnings_count=1, 91 | warnings__contains=["[row : 2] Repeat has no label"], 92 | ) 93 | -------------------------------------------------------------------------------- /tests/test_file.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test file question type. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class FileWidgetTest(PyxformTestCase): 9 | """ 10 | Test file widget class. 11 | """ 12 | 13 | def test_file_type(self): 14 | """ 15 | Test file question type. 16 | """ 17 | self.assertPyxformXform( 18 | name="data", 19 | md=""" 20 | | survey | | | | 21 | | | type | name | label | 22 | | | file | file | Attach a file | 23 | """, 24 | xml__contains=['', ""], 28 | model__contains=[ 29 | """""" 32 | ], 33 | xml__contains=[ 34 | '', 35 | "", 36 | "", 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /tests/test_form_name.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test setting form name to data. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class TestFormName(PyxformTestCase): 9 | def test_default_to_data_when_no_name(self): 10 | """Should default to form_name of 'test_name', and form id of 'data'.""" 11 | self.assertPyxformXform( 12 | md=""" 13 | | survey | | | | 14 | | | type | name | label | 15 | | | text | city | City Name | 16 | """, 17 | instance__contains=[''], 18 | model__contains=[''], 19 | xml__contains=[ 20 | '', 21 | "", 22 | "", 23 | ], 24 | ) 25 | 26 | def test_default_to_data(self): 27 | """ 28 | Test using data as the name of the form which will generate . 29 | """ 30 | self.assertPyxformXform( 31 | md=""" 32 | | survey | | | | 33 | | | type | name | label | 34 | | | text | city | City Name | 35 | """, 36 | name="data", 37 | instance__contains=[''], 38 | model__contains=[''], 39 | xml__contains=[ 40 | '', 41 | "", 42 | "", 43 | ], 44 | ) 45 | 46 | def test_default_form_name_to_superclass_definition(self): 47 | """ 48 | Test no form_name and setting name field, should use name field. 49 | """ 50 | 51 | self.assertPyxformXform( 52 | md=""" 53 | | survey | | | | 54 | | | type | name | label | 55 | | | text | city | City Name | 56 | """, 57 | name="some-name", 58 | instance__contains=[''], 59 | model__contains=[''], 60 | xml__contains=[ 61 | '', 62 | "", 63 | "", 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /tests/test_geo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test geo widgets. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class GeoWidgetsTest(PyxformTestCase): 9 | """Test geo widgets class.""" 10 | 11 | def test_gps_type(self): 12 | self.assertPyxformXform( 13 | name="geo", 14 | md=""" 15 | | survey | | | | 16 | | | type | name | label | 17 | | | gps | location | GPS | 18 | """, 19 | xml__contains=["geopoint"], 20 | ) 21 | 22 | def test_gps_alias(self): 23 | self.assertPyxformXform( 24 | name="geo_alias", 25 | md=""" 26 | | survey | | | | 27 | | | type | name | label | 28 | | | geopoint | location | GPS | 29 | """, 30 | xml__contains=["geopoint"], 31 | ) 32 | 33 | def test_geo_widgets_types(self): 34 | """ 35 | this test could be broken into multiple smaller tests. 36 | """ 37 | self.assertPyxformXform( 38 | name="geos", 39 | md=""" 40 | | survey | | | | 41 | | | type | name | label | 42 | | | begin_repeat | repeat | | 43 | | | geopoint | point | Record Geopoint | 44 | | | note | point_note | Point ${point} | 45 | | | geotrace | trace | Record a Geotrace | 46 | | | note | trace_note | Trace: ${trace} | 47 | | | geoshape | shape | Record a Geoshape | 48 | | | note | shape_note | Shape: ${shape} | 49 | | | end_repeat | | | 50 | """, 51 | xml__contains=[ 52 | "", 53 | "", 54 | "", 55 | "", 56 | "", 57 | "", 58 | '', 59 | '', 61 | '', 62 | '', 64 | '', 65 | '', 67 | ], 68 | ) 69 | -------------------------------------------------------------------------------- /tests/test_group.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing simple cases for Xls2Json 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | from pyxform.builder import create_survey_element_from_dict 8 | from pyxform.xls2json import SurveyReader 9 | 10 | from tests import utils 11 | 12 | 13 | class GroupTests(TestCase): 14 | def test_json(self): 15 | x = SurveyReader(utils.path_to_text_fixture("group.xls"), default_name="group") 16 | x_results = x.to_json_dict() 17 | expected_dict = { 18 | "name": "group", 19 | "title": "group", 20 | "id_string": "group", 21 | "sms_keyword": "group", 22 | "default_language": "default", 23 | "type": "survey", 24 | "children": [ 25 | { 26 | "name": "family_name", 27 | "type": "text", 28 | "label": {"English (en)": "What's your family name?"}, 29 | }, 30 | { 31 | "name": "father", 32 | "type": "group", 33 | "label": {"English (en)": "Father"}, 34 | "children": [ 35 | { 36 | "name": "phone_number", 37 | "type": "phone number", 38 | "label": { 39 | "English (en)": "What's your father's phone number?" 40 | }, 41 | }, 42 | { 43 | "name": "age", 44 | "type": "integer", 45 | "label": {"English (en)": "How old is your father?"}, 46 | }, 47 | ], 48 | }, 49 | { 50 | "children": [ 51 | { 52 | "bind": {"jr:preload": "uid", "readonly": "true()"}, 53 | "name": "instanceID", 54 | "type": "calculate", 55 | } 56 | ], 57 | "control": {"bodyless": True}, 58 | "name": "meta", 59 | "type": "group", 60 | }, 61 | ], 62 | } 63 | self.maxDiff = None 64 | self.assertEqual(x_results, expected_dict) 65 | 66 | def test_equality_of_to_dict(self): 67 | x = SurveyReader(utils.path_to_text_fixture("group.xls"), default_name="group") 68 | x_results = x.to_json_dict() 69 | 70 | survey = create_survey_element_from_dict(x_results) 71 | survey_dict = survey.to_json_dict() 72 | # using the builder sets the title attribute to equal name 73 | # this won't happen through reading the excel file as done by 74 | # SurveyReader. 75 | # Now it happens. 76 | # del survey_dict[u'title'] 77 | self.maxDiff = None 78 | self.assertEqual(x_results, survey_dict) 79 | -------------------------------------------------------------------------------- /tests/test_groups.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test XForm groups. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class GroupsTests(PyxformTestCase): 9 | """ 10 | Test XForm groups. 11 | """ 12 | 13 | def test_group_type(self): 14 | self.assertPyxformXform( 15 | md=""" 16 | | survey | | | | 17 | | | type | name | label | 18 | | | text | pregrp | Pregroup text | 19 | | | begin group | xgrp | XGroup questions | 20 | | | text | xgrp_q1 | XGroup Q1 | 21 | | | integer | xgrp_q2 | XGroup Q2 | 22 | | | end group | | | 23 | | | note | postgrp | Post group note | 24 | """, 25 | model__contains=[ 26 | "", 27 | "", 28 | "", # nopep8 29 | "", # nopep8 30 | "", # nopep8 31 | "", 32 | "", 33 | ], 34 | ) 35 | 36 | def test_group_intent(self): 37 | self.assertPyxformXform( 38 | name="intent_test", 39 | md=""" 40 | | survey | | | | | 41 | | | type | name | label | intent | 42 | | | text | pregrp | Pregroup text | | 43 | | | begin group | xgrp | XGroup questions | ex:org.redcross.openmapkit.action.QUERY(osm_file=${pregrp}) | 44 | | | text | xgrp_q1 | XGroup Q1 | | 45 | | | integer | xgrp_q2 | XGroup Q2 | | 46 | | | end group | | | | 47 | | | note | postgrp | Post group note | | 48 | """, # nopep8 49 | xml__contains=[ 50 | '' # nopep8 51 | ], 52 | ) 53 | 54 | def test_group_relevant_included_in_bind(self): 55 | """Should find the group relevance expression in the group binding.""" 56 | md = """ 57 | | survey | 58 | | | type | name | label | relevant | 59 | | | integer | q1 | Q1 | | 60 | | | begin group | g1 | G1 | ${q1} = 1 | 61 | | | text | q2 | Q2 | | 62 | | | end group | | | | 63 | """ 64 | self.assertPyxformXform( 65 | md=md, 66 | xml__xpath_match=[ 67 | """ 68 | /h:html/h:head/x:model/x:bind[ 69 | @nodeset = '/test_name/g1' and @relevant=' /test_name/q1 = 1' 70 | ] 71 | """ 72 | ], 73 | ) 74 | -------------------------------------------------------------------------------- /tests/test_j2x_creation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing creation of Surveys using verbose methods 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | from pyxform import MultipleChoiceQuestion, Survey, create_survey_from_xls 8 | 9 | from tests import utils 10 | 11 | 12 | class Json2XformVerboseSurveyCreationTests(TestCase): 13 | def test_survey_can_be_created_in_a_slightly_less_verbose_manner(self): 14 | choices = { 15 | "test": [ 16 | {"name": "red", "label": "Red"}, 17 | {"name": "blue", "label": "Blue"}, 18 | ] 19 | } 20 | s = Survey(name="Roses_are_Red", choices=choices) 21 | q = MultipleChoiceQuestion( 22 | name="Favorite_Color", 23 | type="select one", 24 | list_name="test", 25 | ) 26 | s.add_child(q) 27 | 28 | expected_dict = { 29 | "name": "Roses_are_Red", 30 | "type": "survey", 31 | "children": [ 32 | {"name": "Favorite_Color", "type": "select one", "list_name": "test"} 33 | ], 34 | "choices": choices, 35 | } 36 | 37 | self.assertEqual(expected_dict, s.to_json_dict()) 38 | 39 | def test_allow_surveys_with_comment_rows(self): 40 | """assume that a survey with rows that don't have name, type, or label 41 | headings raise warning only""" 42 | path = utils.path_to_text_fixture("allow_comment_rows_test.xls") 43 | survey = create_survey_from_xls(path) 44 | expected_dict = { 45 | "children": [ 46 | { 47 | "label": {"English": "First and last name of farmer"}, 48 | "name": "farmer_name", 49 | "type": "text", 50 | }, 51 | { 52 | "children": [ 53 | { 54 | "bind": {"jr:preload": "uid", "readonly": "true()"}, 55 | "name": "instanceID", 56 | "type": "calculate", 57 | } 58 | ], 59 | "control": {"bodyless": True}, 60 | "name": "meta", 61 | "type": "group", 62 | }, 63 | ], 64 | "default_language": "default", 65 | "id_string": "allow_comment_rows_test", 66 | "name": "data", 67 | "sms_keyword": "allow_comment_rows_test", 68 | "title": "allow_comment_rows_test", 69 | "type": "survey", 70 | } 71 | self.assertEqual(expected_dict, survey.to_json_dict()) 72 | -------------------------------------------------------------------------------- /tests/test_j2x_instantiation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing the instance object for pyxform. 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | from pyxform import Survey, SurveyInstance 8 | from pyxform.builder import create_survey_element_from_dict 9 | 10 | from tests.utils import prep_class_config 11 | 12 | 13 | class Json2XformExportingPrepTests(TestCase): 14 | config = None 15 | cls_name = None 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | prep_class_config(cls=cls) 20 | 21 | def test_simple_survey_instantiation(self): 22 | surv = Survey(name="Simple") 23 | q = create_survey_element_from_dict( 24 | {"type": "text", "name": "survey_question", "label": "Question"} 25 | ) 26 | surv.add_child(q) 27 | 28 | i = surv.instantiate() 29 | 30 | self.assertEqual(i.keys(), ["survey_question"]) 31 | self.assertEqual(set(i.xpaths()), {"/Simple", "/Simple/survey_question"}) 32 | 33 | def test_simple_survey_answering(self): 34 | surv = Survey(name="Water") 35 | q = create_survey_element_from_dict( 36 | {"type": "text", "name": "color", "label": "Color"} 37 | ) 38 | q2 = create_survey_element_from_dict( 39 | {"type": "text", "name": "feeling", "label": "Feeling"} 40 | ) 41 | 42 | surv.add_child(q) 43 | surv.add_child(q2) 44 | i = SurveyInstance(surv) 45 | 46 | i.answer(name="color", value="blue") 47 | self.assertEqual(i.answers()["color"], "blue") 48 | 49 | i.answer(name="feeling", value="liquidy") 50 | self.assertEqual(i.answers()["feeling"], "liquidy") 51 | 52 | def test_answers_can_be_imported_from_xml(self): 53 | surv = Survey(name="data") 54 | 55 | surv.add_child( 56 | create_survey_element_from_dict( 57 | {"type": "text", "name": "name", "label": "Name"} 58 | ) 59 | ) 60 | surv.add_child( 61 | create_survey_element_from_dict( 62 | { 63 | "type": "integer", 64 | "name": "users_per_month", 65 | "label": "Users per month", 66 | } 67 | ) 68 | ) 69 | surv.add_child( 70 | create_survey_element_from_dict( 71 | {"type": "gps", "name": "geopoint", "label": "gps"} 72 | ) 73 | ) 74 | surv.add_child( 75 | create_survey_element_from_dict({"type": "imei", "name": "device_id"}) 76 | ) 77 | 78 | instance = surv.instantiate() 79 | import_xml = self.config.get( 80 | self.cls_name, "test_answers_can_be_imported_from_xml" 81 | ) 82 | instance.import_from_xml(import_xml) 83 | 84 | def test_simple_registration_xml(self): 85 | reg_xform = Survey(name="Registration") 86 | name_question = create_survey_element_from_dict( 87 | {"type": "text", "name": "name", "label": "Name"} 88 | ) 89 | reg_xform.add_child(name_question) 90 | 91 | reg_instance = reg_xform.instantiate() 92 | 93 | reg_instance.answer(name="name", value="bob") 94 | 95 | rx = reg_instance.to_xml() 96 | expected_xml = self.config.get( 97 | self.cls_name, "test_simple_registration_xml" 98 | ).format(reg_xform.id_string) 99 | self.assertEqual(rx, expected_xml) 100 | -------------------------------------------------------------------------------- /tests/test_js2x_import_from_json.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing our ability to import from a JSON text file. 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | from pyxform.builder import ( 8 | create_survey_element_from_dict, 9 | create_survey_element_from_json, 10 | ) 11 | 12 | 13 | class TestJson2XformJsonImport(TestCase): 14 | def test_simple_questions_can_be_imported_from_json(self): 15 | json_text = { 16 | "type": "survey", 17 | "name": "Exchange rate", 18 | "children": [ 19 | { 20 | "label": {"French": "Combien?", "English": "How many?"}, 21 | "type": "decimal", 22 | "name": "exchange_rate", 23 | } 24 | ], 25 | } 26 | s = create_survey_element_from_dict(json_text) 27 | 28 | self.assertEqual(s.children[0].type, "decimal") 29 | 30 | def test_question_type_that_accepts_parameters__without_parameters__to_xml(self): 31 | """Should be able to round-trip survey using a un-parameterised question without error.""" 32 | # Per https://github.com/XLSForm/pyxform/issues/605 33 | # Underlying issue was that the SurveyElement.FIELDS default for "parameters" was 34 | # a string, but in MultipleChoiceQuestion.build_xml a dict is assumed, because 35 | # xls2json.parse_parameters always returns a dict. 36 | js = """ 37 | { 38 | "type": "survey", 39 | "name": "ExchangeRate", 40 | "children": [ 41 | { 42 | "itemset": "pain_locations.xml", 43 | "label": "Location of worst pain this week.", 44 | "name": "pweek", 45 | "type": "select one" 46 | } 47 | ] 48 | } 49 | """ 50 | create_survey_element_from_json(str_or_path=js).to_xml() 51 | -------------------------------------------------------------------------------- /tests/test_json2xform.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing simple cases for pyxform 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | from pyxform.builder import create_survey_element_from_dict 8 | from pyxform.survey import Survey 9 | 10 | # TODO: 11 | # * test_two_questions_with_same_id_fails 12 | # (get this working in json2xform) 13 | 14 | 15 | class BasicJson2XFormTests(TestCase): 16 | def test_survey_can_have_to_xml_called_twice(self): 17 | """ 18 | Test: Survey can have "to_xml" called multiple times 19 | 20 | (This was not being allowed before.) 21 | 22 | It would be good to know (with confidence) that a survey object 23 | can be exported to_xml twice, and the same thing will be returned 24 | both times. 25 | """ 26 | survey = Survey(name="SampleSurvey") 27 | q = create_survey_element_from_dict( 28 | {"type": "text", "name": "name", "label": "label"} 29 | ) 30 | survey.add_child(q) 31 | 32 | str1 = survey.to_xml() 33 | str2 = survey.to_xml() 34 | 35 | self.assertEqual(str1, str2) 36 | -------------------------------------------------------------------------------- /tests/test_language_warnings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test language warnings. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class LanguageWarningTest(PyxformTestCase): 9 | """ 10 | Test language warnings. 11 | """ 12 | 13 | def test_label_with_valid_subtag_should_not_warn(self): 14 | self.assertPyxformXform( 15 | md=""" 16 | | survey | 17 | | | type | name | label::English (en) | label::Acoli (ach) | 18 | | | note | my_note | My note | coc na | 19 | """, 20 | warnings_count=0, 21 | ) 22 | 23 | def test_label_with_no_subtag_should_warn(self): 24 | self.assertPyxformXform( 25 | md=""" 26 | | survey | | | | 27 | | | type | name | label::English | 28 | | | note | my_note | My note | 29 | """, 30 | warnings_count=1, 31 | warnings__contains=[ 32 | "The following language declarations do not contain valid machine-readable " 33 | "codes: English. Learn more: http://xlsform.org#multiple-language-support" 34 | ], 35 | ) 36 | 37 | def test_label_with_unknown_subtag_should_warn(self): 38 | # Bosnian has a short code "bs" so "bos" is not correct per RFC5646. 39 | self.assertPyxformXform( 40 | md=""" 41 | | survey | | | | 42 | | | type | name | label::English (schm) | label::Bosnian (bos) | 43 | | | note | my_note | My note | Moja napomena | 44 | """, 45 | warnings_count=1, 46 | warnings__contains=[ 47 | "The following language declarations do not contain valid machine-readable " 48 | "codes: English (schm), Bosnian (bos). Learn more: http://xlsform.org#multiple-language-support" 49 | ], 50 | ) 51 | 52 | def test_default_language_only_should_not_warn(self): 53 | self.assertPyxformXform( 54 | md=""" 55 | | survey | | | | | 56 | | | type | name | label | choice_filter | 57 | | | select_one opts | opt | My opt | fake = 1 | 58 | | choices| | | | | 59 | | | list_name | name | label | fake | 60 | | | opts | opt1 | Opt1 | 1 | 61 | | | opts | opt2 | Opt2 | 1 | 62 | """, 63 | warnings_count=0, 64 | ) 65 | -------------------------------------------------------------------------------- /tests/test_levenshtein.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pyxform.utils import levenshtein_distance 4 | 5 | """ /* To test in Postgres */ 6 | CREATE EXTENSION IF NOT EXISTS fuzzystrmatch; 7 | SELECT levenshtein(a, b) 8 | FROM ( 9 | VALUES 10 | ('sitting', 'kitten'), 11 | ('Sunday', 'Saturday'), 12 | ('settings', 'settings'), 13 | ('setting', 'settings'), 14 | ('abcdefghijklm', 'nopqrstuvwxyz'), 15 | ('abc klm', '** _rs /wxyz'), 16 | ('ABCD', 'abcd') 17 | ) as t(a, b); 18 | """ 19 | 20 | 21 | class TestLevenshteinDistance(unittest.TestCase): 22 | def test_levenshtein_distance(self): 23 | """Should return the expected distance value.""" 24 | # Verified against Postgres v10 extension "fuzzystrmatch" levenshtein(). 25 | test_data = ( 26 | (3, "sitting", "kitten"), 27 | (3, "Sunday", "Saturday"), 28 | (0, "settings", "settings"), 29 | (1, "setting", "settings"), 30 | (13, "abcdefghijklm", "nopqrstuvwxyz"), 31 | (11, "abc klm", "** _rs /wxyz"), 32 | (4, "ABCD", "abcd"), 33 | ) 34 | for i in test_data: 35 | self.assertEqual(i[0], levenshtein_distance(i[1], i[2]), str(i)) 36 | -------------------------------------------------------------------------------- /tests/test_metadata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test language warnings. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class MetadataTest(PyxformTestCase): 9 | """ 10 | Test metadata and related warnings. 11 | """ 12 | 13 | def test_metadata_bindings(self): 14 | self.assertPyxformXform( 15 | name="metadata", 16 | md=""" 17 | | survey | | | | 18 | | | type | name | label | 19 | | | deviceid | deviceid | | 20 | | | phonenumber | phonenumber | | 21 | | | start | start | | 22 | | | end | end | | 23 | | | today | today | | 24 | | | username | username | | 25 | | | email | email | | 26 | """, 27 | xml__contains=[ 28 | 'jr:preload="property" jr:preloadParams="deviceid"', 29 | 'jr:preload="property" jr:preloadParams="phonenumber"', 30 | 'jr:preload="timestamp" jr:preloadParams="start"', 31 | 'jr:preload="timestamp" jr:preloadParams="end"', 32 | 'jr:preload="date" jr:preloadParams="today"', 33 | 'jr:preload="property" jr:preloadParams="username"', 34 | 'jr:preload="property" jr:preloadParams="email"', 35 | ], 36 | ) 37 | 38 | def test_simserial_deprecation_warning(self): 39 | self.assertPyxformXform( 40 | md=""" 41 | | survey | | | | 42 | | | type | name | label | 43 | | | simserial | simserial | | 44 | | | note | simserial_test_output | simserial_test_output: ${simserial} | 45 | """, 46 | warnings_count=1, 47 | warnings__contains=[ 48 | "[row : 2] simserial is no longer supported on most devices. " 49 | "Only old versions of Collect on Android versions older than 11 still support it." 50 | ], 51 | ) 52 | 53 | def test_subscriber_id_deprecation_warning(self): 54 | self.assertPyxformXform( 55 | md=""" 56 | | survey | | | | 57 | | | type | name | label | 58 | | | subscriberid | subscriberid | sub id - extra warning generated w/o this | 59 | | | note | subscriberid_test_output | subscriberid_test_output: ${subscriberid} | 60 | """, 61 | warnings_count=1, 62 | warnings__contains=[ 63 | "[row : 2] subscriberid is no longer supported on most devices. " 64 | "Only old versions of Collect on Android versions older than 11 still support it." 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /tests/test_output/.test: -------------------------------------------------------------------------------- 1 | This file is here so that the test_output folder exists for a fresh checkout. 2 | The test_output folder is required for some tests to run properly. -------------------------------------------------------------------------------- /tests/test_output/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | PATH = os.path.dirname(__file__) 4 | -------------------------------------------------------------------------------- /tests/test_parameters_rows.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test text rows parameter. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class TestParametersRows(PyxformTestCase): 9 | def test_adding_rows_to_the_body_if_set_in_its_own_column( 10 | self, 11 | ): 12 | self.assertPyxformXform( 13 | name="data", 14 | md=""" 15 | | survey | | | | | 16 | | | type | name | label | body::rows | 17 | | | text | name | Name | 7 | 18 | """, 19 | xml__xpath_match=["/h:html/h:body/x:input[@ref='/data/name' and @rows='7']"], 20 | ) 21 | 22 | def test_using_the_number_of_rows_specified_in_parameters_if_it_is_set_in_both_its_own_column_and_the_parameters_column( 23 | self, 24 | ): 25 | self.assertPyxformXform( 26 | name="data", 27 | md=""" 28 | | survey | | | | | | 29 | | | type | name | label | body::rows | parameters | 30 | | | text | name | Name | 7 | rows=8 | 31 | """, 32 | xml__xpath_match=["/h:html/h:body/x:input[@ref='/data/name' and @rows='8']"], 33 | ) 34 | 35 | def test_adding_rows_to_the_body_if_set_in_parameters( 36 | self, 37 | ): 38 | self.assertPyxformXform( 39 | name="data", 40 | md=""" 41 | | survey | | | | | 42 | | | type | name | label | parameters | 43 | | | text | name | Name | rows=7 | 44 | """, 45 | xml__xpath_match=["/h:html/h:body/x:input[@ref='/data/name' and @rows='7']"], 46 | ) 47 | 48 | def test_throwing_error_if_rows_set_in_parameters_but_the_value_is_not_an_integer( 49 | self, 50 | ): 51 | parameters = ("rows=", "rows=foo", "rows=7.5") 52 | md = """ 53 | | survey | | | | | 54 | | | type | name | label | parameters | 55 | | | text | name | Name | {case} | 56 | """ 57 | for case in parameters: 58 | with self.subTest(msg=case): 59 | self.assertPyxformXform( 60 | name="data", 61 | md=md.format(case=case), 62 | errored=True, 63 | error__contains=[ 64 | "[row : 2] Parameter rows must have an integer value." 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /tests/test_pyxformtestcase.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ensuring that the pyxform_test_case.PyxformTestCase class does some 3 | internal conversions correctly. 4 | """ 5 | 6 | from tests.pyxform_test_case import PyxformTestCase 7 | from tests.xpath_helpers.settings import xps 8 | 9 | 10 | class PyxformTestCaseNonMarkdownSurveyAlternatives(PyxformTestCase): 11 | def test_tainted_vanilla_survey_failure(self): 12 | """ 13 | the _invalid_ss_structure structure should fail to compile 14 | because the note has no label. 15 | 16 | if "errored" parameter is not set to False, it should 17 | raise an exception 18 | """ 19 | _invalid_ss_structure = {"survey": [{"type": "note", "name": "n1"}]} 20 | 21 | def _no_valid_flag(): 22 | """ 23 | when the 'errored' flag is set to false (default) and the survey 24 | fails to compile, the test should raise an exception. 25 | """ 26 | self.assertPyxformXform( 27 | ss_structure=_invalid_ss_structure, 28 | errored=False, # errored=False by default 29 | ) 30 | 31 | self.assertRaises(Exception, _no_valid_flag) 32 | 33 | # however when errored=True is present, 34 | self.assertPyxformXform( 35 | ss_structure=_invalid_ss_structure, 36 | errored=True, 37 | error__contains=["The survey element named 'n1' has no label or hint."], 38 | ) 39 | 40 | def test_vanilla_survey(self): 41 | """ 42 | testing that a survey can be passed as a _spreadsheet structure_ named 43 | 'ss_structure'. 44 | 45 | this will be helpful when testing whitespace constraints and 46 | cell data types since markdown'd surveys strip spaces and 47 | cast empty strings to None values 48 | """ 49 | self.assertPyxformXform( 50 | ss_structure={"survey": [{"type": "note", "name": "n1", "label": "Note 1"}]}, 51 | ) 52 | 53 | 54 | class XlsFormPyxformSurveyTest(PyxformTestCase): 55 | def test_formid_is_not_none(self): 56 | """ 57 | When the form id is not set, it should never use python's 58 | None. Fixing because this messes up other tests. 59 | """ 60 | self.assertPyxformXform( 61 | md=""" 62 | | survey | | | | 63 | | | type | name | label | 64 | | | note | q | Q | 65 | """, 66 | xml__xpath_match=[ 67 | xps.form_id("data"), 68 | ], 69 | ) 70 | -------------------------------------------------------------------------------- /tests/test_rank.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test rank widget. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | from tests.xpath_helpers.choices import xpc 7 | from tests.xpath_helpers.questions import xpq 8 | 9 | 10 | class RangeWidgetTest(PyxformTestCase): 11 | def test_rank(self): 12 | self.assertPyxformXform( 13 | md=""" 14 | | survey | | | | 15 | | | type | name | label | 16 | | | rank mylist | order | Rank | 17 | | choices| | | | 18 | | | list_name | name | label | 19 | | | mylist | a | A | 20 | | | mylist | b | B | 21 | """, 22 | xml__xpath_match=[ 23 | xpc.model_instance_choices_label("mylist", (("a", "A"), ("b", "B"))), 24 | xpq.body_odk_rank_itemset("order"), # also an implicit test for xmlns:odk 25 | "/h:html/h:head/x:model/x:bind[@nodeset='/test_name/order' and @type='odk:rank']", 26 | ], 27 | ) 28 | 29 | def test_rank_filter(self): 30 | self.assertPyxformXform( 31 | name="data", 32 | md=""" 33 | | survey | | | | | 34 | | | type | name | label | choice_filter | 35 | | | rank mylist | order | Rank | color='blue' | 36 | | choices| | | | 37 | | | list_name | name | label | color | 38 | | | mylist | a | A | red | 39 | | | mylist | b | B | blue | 40 | """, 41 | xml__contains=[ 42 | 'xmlns:odk="http://www.opendatakit.org/xforms"', 43 | '', 44 | '', 45 | "red", 46 | "a", 47 | "blue", 48 | "b", 49 | """ 50 | 51 | 52 | 53 | 55 | """, 56 | ], 57 | ) 58 | 59 | def test_rank_translations(self): 60 | """Should find itext/translations for rank, using itemset method.""" 61 | self.assertPyxformXform( 62 | md=""" 63 | | survey | | | | | 64 | | | type | name | label | label::French (fr) | 65 | | | rank mylist | order | Rank | Ranger | 66 | | choices| | | | 67 | | | list_name | name | label | label::French (fr) | 68 | | | mylist | a | A | AA | 69 | | | mylist | b | B | BB | 70 | """, 71 | xml__xpath_match=[ 72 | xpc.model_instance_choices_itext("mylist", ("a", "b")), 73 | xpq.body_odk_rank_itemset("order"), # also an implicit test for xmlns:odk 74 | "/h:html/h:head/x:model/x:bind[@nodeset='/test_name/order' and @type='odk:rank']", 75 | # All itemset translations. 76 | xpc.model_itext_choice_text_label_by_pos("default", "mylist", ("A", "B")), 77 | xpc.model_itext_choice_text_label_by_pos( 78 | "French (fr)", "mylist", ("AA", "BB") 79 | ), 80 | # No non-itemset translations. 81 | xpc.model_itext_no_text_by_id("default", "/test_name/order/a:label"), 82 | xpc.model_itext_no_text_by_id("default", "/test_name/order/b:label"), 83 | xpc.model_itext_no_text_by_id("French (fr)", "/test_name/order/a:label"), 84 | xpc.model_itext_no_text_by_id("French (fr)", "/test_name/order/b:label"), 85 | ], 86 | ) 87 | -------------------------------------------------------------------------------- /tests/test_set_geopoint.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test setgeopoint widget. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class SetGeopointTest(PyxformTestCase): 9 | """Test setgeopoint widget class.""" 10 | 11 | def test_setgeopoint(self): 12 | self.assertPyxformXform( 13 | name="data", 14 | md=""" 15 | | survey | | | | 16 | | | type | name | label | 17 | | | start-geopoint | my-location | my label | 18 | """, 19 | xml__contains=[ 20 | '', 21 | '', 22 | "", 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /tests/test_sms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test sms syntax. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class SMSTest(PyxformTestCase): 9 | def test_prefix_only(self): 10 | self.assertPyxformXform( 11 | name="data", 12 | md=""" 13 | | survey | | | | | 14 | | | type | name | label | hint | 15 | | | string | name | Name | your name | 16 | | settings | | | | | 17 | | | prefix | | | | 18 | | | sms_test | | | | 19 | """, 20 | xml__contains=['odk:prefix="sms_test"'], 21 | ) 22 | 23 | def test_delimiter_only(self): 24 | self.assertPyxformXform( 25 | name="data", 26 | md=""" 27 | | survey | | | | | 28 | | | type | name | label | hint | 29 | | | string | name | Name | your name | 30 | | settings | | | | | 31 | | | delimiter | | | | 32 | | | ~ | | | | 33 | """, 34 | xml__contains=['odk:delimiter="~"'], 35 | ) 36 | 37 | def test_prefix_and_delimiter(self): 38 | self.assertPyxformXform( 39 | name="data", 40 | md=""" 41 | | survey | | | | | 42 | | | type | name | label | hint | 43 | | | string | name | Name | your name | 44 | | settings | | | | | 45 | | | delimiter | prefix | | | 46 | | | * | sms_test2| | | 47 | """, 48 | xml__contains=['odk:delimiter="*"', 'odk:prefix="sms_test2"'], 49 | ) 50 | 51 | def test_sms_tag(self): 52 | self.assertPyxformXform( 53 | name="data", 54 | md=""" 55 | | survey | | | | | | | 56 | | | type | name | compact_tag | label | hint | default | 57 | | | string | name | n | Name | your name | | 58 | | | int | age | +a | Age | your age | 7 | 59 | | | string | fruit | | Fruit | fav fruit | | 60 | """, 61 | xml__contains=[ 62 | '', 63 | '7', 64 | "", 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /tests/test_static_defaults.py: -------------------------------------------------------------------------------- 1 | from tests.pyxform_test_case import PyxformTestCase 2 | 3 | 4 | class StaticDefaultTests(PyxformTestCase): 5 | def test_static_defaults(self): 6 | self.assertPyxformXform( 7 | name="static", 8 | md=""" 9 | | survey | | | | | 10 | | | type | name | label | default | 11 | | | integer | numba | Foo | foo | 12 | | | begin repeat | repeat | | | 13 | | | integer | bar | Bar | 12 | 14 | | | end repeat | repeat | | | 15 | """, 16 | model__contains=["foo", "12"], 17 | model__excludes=["setvalue", ""], 18 | ) 19 | 20 | def test_static_image_defaults(self): 21 | self.assertPyxformXform( 22 | name="static_image", 23 | md=""" 24 | | survey | | | | | | 25 | | | type | name | label | parameters | default | 26 | | | image | my_image | Image | max-pixels=640 | my_default_image.jpg | 27 | | | text | my_descr | descr | | no description provied | 28 | """, 29 | model__contains=[ 30 | # image needed NS and question typing still exist! 31 | 'xmlns:orx="http://openrosa.org/xforms"', 32 | '', 33 | # image default appears 34 | "jr://images/my_default_image.jpg", 35 | # other defaults appear 36 | "no description provied", 37 | ], 38 | model__excludes=[ 39 | "setvalue", 40 | "", 41 | "", 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /tests/test_survey.py: -------------------------------------------------------------------------------- 1 | from pyxform.question import InputQuestion 2 | from pyxform.survey import Survey 3 | 4 | from tests.pyxform_test_case import PyxformTestCase 5 | 6 | 7 | class TestSurvey(PyxformTestCase): 8 | """ 9 | Tests for the Survey class. 10 | """ 11 | 12 | def test_many_xpath_references_do_not_hit_64_recursion_limit__one_to_one(self): 13 | """Should be able to pipe a question into one note more than 64 times.""" 14 | self.assertPyxformXform( 15 | md=""" 16 | | survey | | | | | 17 | | | type | name | label | relevant | 18 | | | text | q1 | Q1 | | 19 | | | note | n | {n} | | 20 | | | text | q2 | Q2 | {r} | 21 | """.format(n="q1 = ${q1} " * 250, r=" or ".join(["${q1} = 'y'"] * 250)), 22 | ) 23 | 24 | def test_many_xpath_references_do_not_hit_64_recursion_limit__many_to_one(self): 25 | """Should be able to pipe more than 64 questions into one note.""" 26 | tmpl_q = "| | text | q{0} | Q{0} |" 27 | tmpl_n = "q{0} = ${{q{0}}} " 28 | self.assertPyxformXform( 29 | md=""" 30 | | survey | | | | 31 | | | type | name | label | 32 | {q} 33 | | | note | n | {n} | 34 | """.format( 35 | q="\n".join(tmpl_q.format(i) for i in range(1, 250)), 36 | n=" ".join(tmpl_n.format(i) for i in range(1, 250)), 37 | ), 38 | ) 39 | 40 | def test_many_xpath_references_do_not_hit_64_recursion_limit__many_to_many(self): 41 | """Should be able to pipe more than 64 questions into 64 notes.""" 42 | tmpl_q = "| | text | q{0} | Q{0} |" 43 | tmpl_n = "| | note | n{0} | q{0} = ${{q{0}}} |" 44 | self.assertPyxformXform( 45 | md=""" 46 | | survey | | | | 47 | | | type | name | label | 48 | {q} 49 | {n} 50 | """.format( 51 | q="\n".join(tmpl_q.format(i) for i in range(1, 250)), 52 | n="\n".join(tmpl_n.format(i) for i in range(1, 250)), 53 | ), 54 | ) 55 | 56 | def test_autoplay_attribute_added_to_question_body_control(self): 57 | """Should add the autoplay attribute when specified for a question.""" 58 | md = """ 59 | | survey | 60 | | | type | name | label | audio | autoplay | 61 | | | text | feel | Song feel? | amazing.mp3 | audio | 62 | """ 63 | self.assertPyxformXform( 64 | md=md, 65 | xml__xpath_match=[ 66 | """ 67 | /h:html/h:body/x:input[@ref='/test_name/feel' and @autoplay='audio'] 68 | """ 69 | ], 70 | ) 71 | 72 | def test_xpath_dict_initialised_once(self): 73 | """Should be able to convert a valid form to XML repeatedly with the same result.""" 74 | s = Survey(name="guest_list") 75 | s.add_children( 76 | [ 77 | InputQuestion(name="q1", type="text", label="Your first name?"), 78 | InputQuestion(name="q2", type="text", label="${q1}, last name?"), 79 | ] 80 | ) 81 | s._setup_xpath_dictionary() 82 | # If the dict is re-initialised, "duplicate" elements will be found, which 83 | # results in the value being set to None. 84 | self.assertFalse(any(i for i in s._xpath.values() if i is None)) 85 | # Due to the pyxform reference, _var_repl_function() raises an error for None, 86 | # so calling to_xml() twice would trigger a "duplicates" error. 87 | self.assertEqual(s.to_xml(validate=False), s.to_xml(validate=False)) 88 | -------------------------------------------------------------------------------- /tests/test_survey_element.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from unittest import TestCase 3 | 4 | from pyxform.survey_element import SurveyElement 5 | 6 | 7 | class TestSurveyElementMappingBehaviour(TestCase): 8 | def tearDown(self): 9 | # Undo the warnings filter set in the below test. 10 | warnings.resetwarnings() 11 | 12 | def test_get_call_patterns_equivalent_to_base_dict(self): 13 | """Should find that, except for deprecated usage, SurveyElement is dict-like.""" 14 | # To demonstrate how dict normally works using same test cases. 15 | _dict = {"name": "test", "label": None} 16 | # getattr 17 | self.assertEqual("default", getattr(_dict, "foo", "default")) 18 | # defined key, no default 19 | self.assertEqual("test", _dict.get("name")) 20 | # defined key, with default 21 | self.assertEqual("test", _dict.get("name", "default")) 22 | # defined key, with None value 23 | self.assertEqual(None, _dict.get("label")) 24 | # defined key, with None value, with default 25 | self.assertEqual(None, _dict.get("label", "default")) 26 | # undefined key, with default 27 | self.assertEqual("default", _dict.get("foo", "default")) 28 | # undefined key, with default None 29 | self.assertEqual(None, _dict.get("foo", None)) 30 | # other access patterns for undefined key 31 | self.assertEqual(None, _dict.get("foo")) 32 | with self.assertRaises(AttributeError): 33 | _ = _dict.foo 34 | with self.assertRaises(KeyError): 35 | _ = _dict["foo"] 36 | 37 | elem = SurveyElement(name="test") 38 | # getattr 39 | self.assertEqual("default", getattr(elem, "foo", "default")) 40 | # defined key, no default 41 | self.assertEqual("test", elem.get("name")) 42 | # defined key, with default 43 | self.assertEqual("test", elem.get("name", "default")) 44 | # defined key, with None value 45 | self.assertEqual(None, elem.get("label")) 46 | # defined key, with None value, with default 47 | self.assertEqual(None, elem.get("label", "default")) 48 | # undefined key, with default 49 | with self.assertWarns(DeprecationWarning) as warned: 50 | self.assertEqual("default", elem.get("foo", "default")) 51 | # Warning points to caller, rather than survey_element or collections.abc. 52 | self.assertEqual(__file__, warned.filename) 53 | # undefined key, with default None 54 | with self.assertWarns(DeprecationWarning) as warned: 55 | self.assertEqual(None, elem.get("foo", None)) 56 | # Callers can disable warnings at module-level. 57 | warnings.simplefilter("ignore", DeprecationWarning) 58 | with warnings.catch_warnings(record=True) as warned: 59 | elem.get("foo", "default") 60 | self.assertEqual(0, len(warned)) 61 | # other access patterns for undefined key 62 | with self.assertRaises(AttributeError): 63 | _ = elem.get("foo") 64 | with self.assertRaises(AttributeError): 65 | _ = elem.foo 66 | with self.assertRaises(AttributeError): 67 | _ = elem["foo"] 68 | -------------------------------------------------------------------------------- /tests/test_table_list.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test table list appearance syntax. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | MD = ''' 8 | | survey | | | | | | 9 | | | type | name | label | hint |appearance | 10 | | | begin_group | tablelist1 | Table_Y_N | |table-list minimal | 11 | | | select_one yes_no | options1a | Q1 | first row! | | 12 | | | select_one yes_no | options1b | Q2 | | | 13 | | | end_group | | | | | 14 | | choices | | | | | | 15 | | | list_name | name | label | | | 16 | | | yes_no | yes | Yes | | | 17 | 0 """ # noqa 18 | ''' # nopep8 19 | 20 | XML_CONTAINS = """ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | first row! 35 | """.strip() # nopep8 36 | 37 | 38 | class TableListTest(PyxformTestCase): 39 | def test_table_list(self): 40 | self.assertPyxformXform( 41 | name="table-list-appearance-mod", 42 | md=MD, 43 | xml__contains=[XML_CONTAINS], 44 | ) 45 | -------------------------------------------------------------------------------- /tests/test_tutorial_xls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test tutorial XLSForm. 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | from pyxform.builder import create_survey_from_path 8 | 9 | from tests import utils 10 | 11 | 12 | class TutorialTests(TestCase): 13 | @staticmethod 14 | def test_create_from_path(): 15 | path = utils.path_to_text_fixture("tutorial.xls") 16 | create_survey_from_path(path) 17 | -------------------------------------------------------------------------------- /tests/test_unicode_rtl.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test unicode rtl in XLSForms. 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class UnicodeStrings(PyxformTestCase): 9 | def test_unicode_snowman(self): 10 | self.assertPyxformXform( 11 | md=""" 12 | | survey | | | | 13 | | | type | name | label | 14 | | | text | snowman | ☃ | 15 | """, 16 | xml__contains=[""], 17 | ) 18 | 19 | def test_smart_quotes(self): 20 | self.assertPyxformXform( 21 | ss_structure={ 22 | "survey": [ 23 | { 24 | "type": "select_one xyz", 25 | "name": "smart_single_quoted", 26 | "label": """ 27 | ‘single-quoted’ 28 | """.strip(), 29 | }, 30 | { 31 | "type": "text", 32 | "name": "smart_double_quoted", 33 | "relevant": "selected(${smart_single_quoted}, ‘xxx’)", 34 | "label": """ 35 | “double-quoted” 36 | """.strip(), 37 | }, 38 | { 39 | "type": "integer", 40 | "name": "my_default_is_123", 41 | "label": "my default is 123", 42 | "default": "123", 43 | }, 44 | ], 45 | "choices": [ 46 | {"list_name": "xyz", "name": "xxx", "label": "‘Xxx’"}, 47 | {"list_name": "xyz", "name": "yyy", "label": "“Yyy”"}, 48 | ], 49 | "settings": [{"version": "q(‘-’)p"}], 50 | }, 51 | name="quoth", 52 | xml__contains=[ 53 | "'single-quoted", 54 | '"double-quoted"', 55 | "selected( /quoth/smart_single_quoted , 'xxx')", 56 | "123", 57 | "", 58 | '', 59 | """ 60 | version="q('-')p" 61 | """.strip(), 62 | ], 63 | ) 64 | -------------------------------------------------------------------------------- /tests/test_upload_question.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test upload (image, audio, file) question types in XLSForm 3 | """ 4 | 5 | from tests.pyxform_test_case import PyxformTestCase 6 | 7 | 8 | class UploadTest(PyxformTestCase): 9 | def test_image_question(self): 10 | self.assertPyxformXform( 11 | name="data", 12 | md=""" 13 | | survey | | | | 14 | | | type | name | label | 15 | | | image | photo | Take a photo: | 16 | """, 17 | xml__contains=[ 18 | '', 19 | '', 20 | "", 21 | "", 22 | ], 23 | ) 24 | 25 | def test_audio_question(self): 26 | self.assertPyxformXform( 27 | name="data", 28 | md=""" 29 | | survey | | | | 30 | | | type | name | label | 31 | | | audio | recording1 | Record a sound: | 32 | """, 33 | xml__contains=[ 34 | '', 35 | '', 36 | "", 37 | "", 38 | ], 39 | ) 40 | 41 | def test_file_question(self): 42 | self.assertPyxformXform( 43 | name="data", 44 | md=""" 45 | | survey | | | | 46 | | | type | name | label | 47 | | | file | file1 | Upload a file: | 48 | """, 49 | xml__contains=[ 50 | '', 51 | '', 52 | "", 53 | "", 54 | ], 55 | ) 56 | 57 | def test_file_question_restrict_filetype(self): 58 | self.assertPyxformXform( 59 | name="data", 60 | md=""" 61 | | survey | | | | | 62 | | | type | name | label | body::accept | 63 | | | file | upload_a_pdf | Upload a PDF: | application/pdf | 64 | """, 65 | xml__contains=['"], 18 | model__contains=[''], 19 | xml__contains=[ 20 | '', 21 | "a hint", # nopep8 22 | "", 23 | ], 24 | ) 25 | 26 | def test_l2(self): 27 | self.assertPyxformXform( 28 | name="img_test", 29 | md=""" 30 | | survey | | | | 31 | | | type | name | image | 32 | | | note | display_img_test | img_test.jpg | 33 | """, 34 | model__contains=[ 35 | '' 37 | ], 38 | instance__contains=[""], 39 | xml__contains=[ 40 | '', 41 | # and further down... 42 | """