hap-cli 0.8.17__tar.gz → 0.8.19__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hap_cli-0.8.17 → hap_cli-0.8.19}/PKG-INFO +1 -1
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/__init__.py +1 -1
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +55 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/record.py +130 -22
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/worksheet_templates.py +55 -0
- hap_cli-0.8.19/hap_cli/tests/test_member_relation_array_wrap.py +103 -0
- hap_cli-0.8.19/hap_cli/tests/test_select_rejects_relation_value.py +178 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/PKG-INFO +1 -1
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/SOURCES.txt +2 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/MANIFEST.in +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/README.md +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/__main__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/config.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/harness.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/smoke.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/state.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/README.md +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/README_CN.md +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/__main__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/app_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/app_creator_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/app_editor_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/auth_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/calendar_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/chart_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/chat_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/chatbot_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/config_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/contact_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/department_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/group_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/icon_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/instance_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/node_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/optionset_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/page_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/post_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/record_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/region_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/role_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/update_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/upload_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/v3_registry.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/workflow_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/worksheet_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/context.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/action_spec_adapter.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/charts.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/cleanup.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/compiler.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/config.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/design/design.schema.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/errors.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/executor.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/fields.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/hap.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/merge.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/recording/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/recording/console.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/recording/mirror.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/recording/report.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/schema.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/executor.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/template.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/selftest.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/smoke.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/steps.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/store.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/validate.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/auth.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/calendar_mod.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/chart.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/chat.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/chatbot.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/cli_access.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/contact.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/control_type_codes.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/department.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/apply.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/componentlower.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/editspec.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/errors.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/fieldlower.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/jsonschema_mini.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/models.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/ops.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/planner.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/reader.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/field_normalizer.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/field_spec_adapter.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/filter_translator.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/flow_node.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/global_meta.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/group.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/icon_index.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/instance.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/logger.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/optionset.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/page.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/post.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/response_crypto.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/role.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/role_perm_builder.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/session.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/token_crypto.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/update.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/upload.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/v3/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/v3/dispatcher.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/v3/render.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/v3/schema.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/view_spec_adapter.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/workflow.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/workflow_node_dsl.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/worksheet.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/hap_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/i18n.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/locale/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/locale/messages.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/locale/messages.schema.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/conftest.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_app_creator.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_app_editor.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_app_export_usage.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_auth_prerelease.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_bug_036430.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_bug_036750.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_chart.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_cli_access.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_config_cmd.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_core.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_full_e2e.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_global_meta.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_i18n.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_instance_history_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_approval.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_approval_actions.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_calendar.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_destructive.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_misc.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_post.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_social.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_v3.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_workflow.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_multi_profile.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_org_id_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_org_id_docs.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_parameter_conventions.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_record_field_alias.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_semantic_types_registry.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_update.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_v3_dispatcher.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_v3_session.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_workflow_list_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/utils/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/utils/formatting.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/utils/options.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/utils/parameter_mapping.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/dependency_links.txt +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/entry_points.txt +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/requires.txt +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/top_level.txt +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/__main__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/_wftest.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/charts.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/cleanup.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/compiler.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/config.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/errors.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/executor.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/fields.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/hap.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/recording/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/recording/console.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/recording/jsonl.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/recording/mirror.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/recording/report.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/schema.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/seed/__init__.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/seed/cli.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/seed/executor.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/seed/template.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/selftest.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/smoke.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/step.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/steps.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/store.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/workflow_dsl.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/setup.cfg +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/setup.py +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/skills/cli/hap-cli-app-editor/scripts/editspec/component.schema.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/skills/cli/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/skills/cli/hap-cli-app-editor/scripts/editspec/envelope.schema.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/skills/cli/hap-cli-app-editor/scripts/editspec/field.schema.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/INDEX.json +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/create_optionset.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/create_role.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/delete_optionset.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/delete_record.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/delete_role.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_app_info.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_details.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_list.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_logs.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_relations.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_regions.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_role_details.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_role_list.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/knowledge_search.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
- {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/update_optionset.yaml +0 -0
|
@@ -486,6 +486,34 @@ def build_control(
|
|
|
486
486
|
return ctrl
|
|
487
487
|
|
|
488
488
|
|
|
489
|
+
# The fields a HAP select option may legitimately carry. The base shape
|
|
490
|
+
# is {key, value, isDeleted, index, checked, color} (pd-openweb
|
|
491
|
+
# src/pages/widgetConfig/util/setting.js getDefaultOptions); ``score`` is
|
|
492
|
+
# the optional NPS/scoring weight (OptionList/AssignValue.jsx). There is
|
|
493
|
+
# no ``type`` field — an option carrying one is malformed input, not a HAP
|
|
494
|
+
# shape, so it is rejected rather than silently dropped.
|
|
495
|
+
_ALLOWED_OPTION_FIELDS = frozenset(
|
|
496
|
+
{"key", "value", "isDeleted", "index", "checked", "color", "score"}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _option_value_is_object_shaped(v: Any) -> bool:
|
|
501
|
+
"""True when an option key/value is a relation/member/object shape
|
|
502
|
+
(``[{"sid": …}]`` / ``{...}``) instead of plain text — which would
|
|
503
|
+
only surface server-side as a ``ControlOptions`` deserialization
|
|
504
|
+
failure on the batch save."""
|
|
505
|
+
if isinstance(v, (list, tuple, dict)):
|
|
506
|
+
return True
|
|
507
|
+
if isinstance(v, str):
|
|
508
|
+
s = v.strip()
|
|
509
|
+
if s[:1] in ("[", "{"):
|
|
510
|
+
try:
|
|
511
|
+
return isinstance(json.loads(s), (list, dict))
|
|
512
|
+
except (ValueError, TypeError):
|
|
513
|
+
return False
|
|
514
|
+
return False
|
|
515
|
+
|
|
516
|
+
|
|
489
517
|
def _normalize_options(
|
|
490
518
|
opts: list[str] | list[dict[str, Any]],
|
|
491
519
|
) -> list[dict[str, Any]]:
|
|
@@ -493,6 +521,10 @@ def _normalize_options(
|
|
|
493
521
|
|
|
494
522
|
First item is marked as the default selection (checked=True) unless
|
|
495
523
|
any caller-supplied dict already sets it.
|
|
524
|
+
|
|
525
|
+
Option ``value``/``key`` must be plain text; a relation/object shape
|
|
526
|
+
or an unsupported field (e.g. ``type``) is rejected up front rather
|
|
527
|
+
than silently mangled or dropped.
|
|
496
528
|
"""
|
|
497
529
|
colors = [
|
|
498
530
|
"#C9E6FC", "#C3F2F2", "#C2F1D2", "#FFE7B1", "#FBD2BF",
|
|
@@ -502,6 +534,27 @@ def _normalize_options(
|
|
|
502
534
|
any_default = any(isinstance(o, dict) and o.get("checked") for o in opts)
|
|
503
535
|
for i, o in enumerate(opts):
|
|
504
536
|
if isinstance(o, dict):
|
|
537
|
+
unknown = set(o) - _ALLOWED_OPTION_FIELDS
|
|
538
|
+
if unknown:
|
|
539
|
+
raise ValueError(
|
|
540
|
+
f"Select option {o!r} has unsupported field(s) "
|
|
541
|
+
f"{sorted(unknown)}; an option only accepts "
|
|
542
|
+
f"{sorted(_ALLOWED_OPTION_FIELDS)}."
|
|
543
|
+
)
|
|
544
|
+
if o.get("value") in (None, ""):
|
|
545
|
+
raise ValueError(
|
|
546
|
+
f"Select option {o!r} is missing a non-empty 'value'."
|
|
547
|
+
)
|
|
548
|
+
if _option_value_is_object_shaped(o.get("value")):
|
|
549
|
+
raise ValueError(
|
|
550
|
+
f"Select option 'value' must be plain text, got a "
|
|
551
|
+
f"relation/object-shaped value: {o.get('value')!r}."
|
|
552
|
+
)
|
|
553
|
+
if o.get("key") is not None and _option_value_is_object_shaped(o.get("key")):
|
|
554
|
+
raise ValueError(
|
|
555
|
+
f"Select option 'key' must be a string, got a "
|
|
556
|
+
f"relation/object-shaped value: {o.get('key')!r}."
|
|
557
|
+
)
|
|
505
558
|
entry = {
|
|
506
559
|
"key": o.get("key") or str(uuid.uuid4()),
|
|
507
560
|
"value": o["value"],
|
|
@@ -510,6 +563,8 @@ def _normalize_options(
|
|
|
510
563
|
"checked": o.get("checked", False),
|
|
511
564
|
"color": o.get("color", colors[i % len(colors)]),
|
|
512
565
|
}
|
|
566
|
+
if "score" in o:
|
|
567
|
+
entry["score"] = o["score"]
|
|
513
568
|
else:
|
|
514
569
|
entry = {
|
|
515
570
|
"key": str(uuid.uuid4()),
|
|
@@ -22,13 +22,56 @@ DISCUSSION_SOURCE_WORKSHEET_ROW = 8
|
|
|
22
22
|
_LEGACY_SKIP_TYPES = {30, 31, 32, 33, 37, 38, 53}
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _option_field_label(ctrl: dict[str, Any]) -> str:
|
|
26
|
+
return str(ctrl.get("controlName") or ctrl.get("controlId") or "option field")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _reject_relation_shaped_option(ctrl: dict[str, Any], item: Any) -> None:
|
|
30
|
+
"""Fail loudly when a select cell is handed a relation/member/sub-table
|
|
31
|
+
*object* shape instead of an option value.
|
|
32
|
+
|
|
33
|
+
A single/multi-select cell legitimately carries only an option key, a
|
|
34
|
+
known label, or an ``add_<text>`` new-option directive — never a
|
|
35
|
+
``[{"sid": rowId}]`` / ``{...}`` structure. When such a value slips
|
|
36
|
+
through (e.g. row data maps a relation value to the wrong child field),
|
|
37
|
+
the old code passed it straight to the server, which then failed
|
|
38
|
+
deserializing ``ControlOptions`` on the batch save. Catching it here
|
|
39
|
+
points the blame at the offending field instead of a generic 500.
|
|
40
|
+
"""
|
|
41
|
+
looks_object = isinstance(item, (list, tuple, dict))
|
|
42
|
+
if not looks_object and isinstance(item, str):
|
|
43
|
+
stripped = item.strip()
|
|
44
|
+
if stripped[:1] in ("[", "{"):
|
|
45
|
+
try:
|
|
46
|
+
parsed = json.loads(stripped)
|
|
47
|
+
except (ValueError, TypeError):
|
|
48
|
+
parsed = None
|
|
49
|
+
looks_object = isinstance(parsed, (list, dict))
|
|
50
|
+
if looks_object:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Field '{_option_field_label(ctrl)}' is a select field and "
|
|
53
|
+
f"expects an option key, a label, or an 'add_<text>' new-option "
|
|
54
|
+
f"value, but got a relation/object-shaped value: {item!r}. "
|
|
55
|
+
f"This value belongs to a relation or member field, not this "
|
|
56
|
+
f"select field — check the field mapping."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
25
60
|
def _resolve_option_keys(ctrl: dict[str, Any], raw: Any) -> list[str]:
|
|
26
61
|
"""Map option labels (or already-keys) to the option `key` values
|
|
27
|
-
the legacy AddWorksheetRow expects (it takes keys, not labels).
|
|
62
|
+
the legacy AddWorksheetRow expects (it takes keys, not labels).
|
|
63
|
+
|
|
64
|
+
Relation/member/sub-table *object* shapes (``[{"sid": …}]`` etc.) are
|
|
65
|
+
rejected up front — they are never valid option values and would only
|
|
66
|
+
surface server-side as a ``ControlOptions`` deserialization failure on
|
|
67
|
+
the batch save.
|
|
68
|
+
"""
|
|
28
69
|
if raw is None or raw == "":
|
|
29
70
|
return []
|
|
30
71
|
if isinstance(raw, (list, tuple)):
|
|
31
|
-
items =
|
|
72
|
+
items: list[Any] = list(raw)
|
|
73
|
+
elif isinstance(raw, dict):
|
|
74
|
+
items = [raw]
|
|
32
75
|
else:
|
|
33
76
|
items = [s for s in str(raw).split(",") if s != ""]
|
|
34
77
|
by_label: dict[str, str] = {}
|
|
@@ -40,7 +83,9 @@ def _resolve_option_keys(ctrl: dict[str, Any], raw: Any) -> list[str]:
|
|
|
40
83
|
by_label[str(opt.get("value", ""))] = key
|
|
41
84
|
out: list[str] = []
|
|
42
85
|
for it in items:
|
|
43
|
-
|
|
86
|
+
_reject_relation_shaped_option(ctrl, it)
|
|
87
|
+
s = str(it)
|
|
88
|
+
out.append(s if s in valid_keys else by_label.get(s, s))
|
|
44
89
|
return out
|
|
45
90
|
|
|
46
91
|
|
|
@@ -214,6 +259,67 @@ def _legacy_cell_entry(
|
|
|
214
259
|
return {"value": _legacy_cell_value(ctrl, value)}
|
|
215
260
|
|
|
216
261
|
|
|
262
|
+
def _coerce_entity_ids(
|
|
263
|
+
value: Any, id_keys: tuple[str, ...], *, field_label: str,
|
|
264
|
+
) -> list[str]:
|
|
265
|
+
"""Flatten a member / department / org-role / relation cell value to a
|
|
266
|
+
list of id strings, ready to be wrapped as ``[{idKey: id}, ...]``.
|
|
267
|
+
|
|
268
|
+
Accepts every realistic input shape so callers never have to pre-shape:
|
|
269
|
+
* a scalar id, or comma-separated ids (``"a,b"``);
|
|
270
|
+
* a list/tuple mixing ids and ``{idKey: ...}`` objects;
|
|
271
|
+
* a single ``{idKey: ...}`` object — the ``get_records`` read-back
|
|
272
|
+
shape carries display fields (``fullname`` / ``avatar`` / ``name``)
|
|
273
|
+
too, so only the id is kept;
|
|
274
|
+
* a JSON-array/object *string* of the above (e.g. a value copied
|
|
275
|
+
straight out of a record dump).
|
|
276
|
+
|
|
277
|
+
``id_keys`` lists the accepted id field names in priority order, e.g.
|
|
278
|
+
``("accountId", "aid")`` for USER_PICKER. An object that carries none of
|
|
279
|
+
them fails loudly instead of being ``str()``-mangled into a junk id —
|
|
280
|
+
that silent mangling sent garbage members to the server before.
|
|
281
|
+
"""
|
|
282
|
+
if value is None or value == "":
|
|
283
|
+
return []
|
|
284
|
+
# A JSON-array/object string is parsed structurally rather than
|
|
285
|
+
# comma-split (a member object's JSON has commas inside it).
|
|
286
|
+
if isinstance(value, str):
|
|
287
|
+
s = value.strip()
|
|
288
|
+
if s[:1] in ("[", "{"):
|
|
289
|
+
try:
|
|
290
|
+
value = json.loads(s)
|
|
291
|
+
except (ValueError, TypeError):
|
|
292
|
+
pass
|
|
293
|
+
if isinstance(value, dict):
|
|
294
|
+
items: list[Any] = [value]
|
|
295
|
+
elif isinstance(value, (list, tuple)):
|
|
296
|
+
items = list(value)
|
|
297
|
+
else:
|
|
298
|
+
items = [p for p in str(value).split(",") if p.strip() != ""]
|
|
299
|
+
out: list[str] = []
|
|
300
|
+
for it in items:
|
|
301
|
+
if isinstance(it, dict):
|
|
302
|
+
rid = next(
|
|
303
|
+
(str(it[k]) for k in id_keys if it.get(k) not in (None, "")),
|
|
304
|
+
None,
|
|
305
|
+
)
|
|
306
|
+
if rid is None:
|
|
307
|
+
raise ValueError(
|
|
308
|
+
f"Field '{field_label}': object value is missing an id "
|
|
309
|
+
f"field ({'/'.join(id_keys)}): {it!r}"
|
|
310
|
+
)
|
|
311
|
+
out.append(rid)
|
|
312
|
+
elif isinstance(it, (list, tuple)):
|
|
313
|
+
raise ValueError(
|
|
314
|
+
f"Field '{field_label}': nested array is not a valid id: {it!r}"
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
s2 = str(it).strip()
|
|
318
|
+
if s2:
|
|
319
|
+
out.append(s2)
|
|
320
|
+
return out
|
|
321
|
+
|
|
322
|
+
|
|
217
323
|
def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
|
|
218
324
|
"""Serialize one cell to the legacy receiveControls `value`
|
|
219
325
|
string, per the shapes proven in
|
|
@@ -226,14 +332,15 @@ def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
|
|
|
226
332
|
if t == 10: # MULTI_SELECT
|
|
227
333
|
return json.dumps(_resolve_option_keys(ctrl, value), ensure_ascii=False)
|
|
228
334
|
|
|
335
|
+
label = _option_field_label(ctrl)
|
|
336
|
+
|
|
229
337
|
if t in (29, 35): # RELATE_SHEET / CASCADING_SELECT -> [{"sid": rowId}, ...]
|
|
230
338
|
# Cascade (35) is a relation to a row in another worksheet, persisted
|
|
231
339
|
# the same way as a plain relate (29): an array of {"sid": rowId}. A
|
|
232
|
-
# bare rowId makes the server answer "服务异常" (bug #642cc8).
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
ids = [s for s in str(value).split(",") if s != ""]
|
|
340
|
+
# bare rowId makes the server answer "服务异常" (bug #642cc8). Object
|
|
341
|
+
# inputs ({"sid": rowId, ...} read-back shape) keep only the sid.
|
|
342
|
+
ids = _coerce_entity_ids(value, ("sid", "rowid", "rowId"),
|
|
343
|
+
field_label=label)
|
|
237
344
|
return json.dumps([{"sid": rid} for rid in ids], ensure_ascii=False)
|
|
238
345
|
|
|
239
346
|
if t == 48: # ORG_ROLE -> [{"organizeId": id}, ...]
|
|
@@ -241,20 +348,21 @@ def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
|
|
|
241
348
|
# of {"organizeId": id} objects; organizeName is display-only and the
|
|
242
349
|
# server resolves it from the id. A bare organizeId is rejected with
|
|
243
350
|
# "服务异常" (bug #642cc8). See pd-openweb CellControls/OrgRole.jsx.
|
|
244
|
-
ids =
|
|
245
|
-
|
|
246
|
-
return json.dumps([{"organizeId":
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
351
|
+
ids = _coerce_entity_ids(value, ("organizeId", "id"),
|
|
352
|
+
field_label=label)
|
|
353
|
+
return json.dumps([{"organizeId": i} for i in ids], ensure_ascii=False)
|
|
354
|
+
|
|
355
|
+
if t == 26: # USER_PICKER -> always an array of {"accountId": id}, even
|
|
356
|
+
# for a single member. Object inputs (the full member object the UI
|
|
357
|
+
# holds / get_records returns: {accountId, fullname, avatar, ...})
|
|
358
|
+
# are reduced to their accountId — a bare object would fail the save.
|
|
359
|
+
ids = _coerce_entity_ids(value, ("accountId", "aid"),
|
|
360
|
+
field_label=label)
|
|
361
|
+
return json.dumps([{"accountId": i} for i in ids], ensure_ascii=False)
|
|
362
|
+
if t == 27: # DEPARTMENT -> [{"departmentId": id}, ...]
|
|
363
|
+
ids = _coerce_entity_ids(value, ("departmentId", "id"),
|
|
364
|
+
field_label=label)
|
|
365
|
+
return json.dumps([{"departmentId": i} for i in ids],
|
|
258
366
|
ensure_ascii=False)
|
|
259
367
|
|
|
260
368
|
if t == 36: # SWITCH / checkbox
|
|
@@ -471,6 +471,34 @@ def build_control(
|
|
|
471
471
|
return ctrl
|
|
472
472
|
|
|
473
473
|
|
|
474
|
+
# The fields a HAP select option may legitimately carry. The base shape
|
|
475
|
+
# is {key, value, isDeleted, index, checked, color} (pd-openweb
|
|
476
|
+
# src/pages/widgetConfig/util/setting.js getDefaultOptions); ``score`` is
|
|
477
|
+
# the optional NPS/scoring weight (OptionList/AssignValue.jsx). There is
|
|
478
|
+
# no ``type`` field — an option carrying one is malformed input, not a HAP
|
|
479
|
+
# shape, so it is rejected rather than silently dropped.
|
|
480
|
+
_ALLOWED_OPTION_FIELDS = frozenset(
|
|
481
|
+
{"key", "value", "isDeleted", "index", "checked", "color", "score"}
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _option_value_is_object_shaped(v: Any) -> bool:
|
|
486
|
+
"""True when an option key/value is a relation/member/object shape
|
|
487
|
+
(``[{"sid": …}]`` / ``{...}``) instead of plain text — which would
|
|
488
|
+
only surface server-side as a ``ControlOptions`` deserialization
|
|
489
|
+
failure on the batch save."""
|
|
490
|
+
if isinstance(v, (list, tuple, dict)):
|
|
491
|
+
return True
|
|
492
|
+
if isinstance(v, str):
|
|
493
|
+
s = v.strip()
|
|
494
|
+
if s[:1] in ("[", "{"):
|
|
495
|
+
try:
|
|
496
|
+
return isinstance(json.loads(s), (list, dict))
|
|
497
|
+
except (ValueError, TypeError):
|
|
498
|
+
return False
|
|
499
|
+
return False
|
|
500
|
+
|
|
501
|
+
|
|
474
502
|
def _normalize_options(
|
|
475
503
|
opts: list[str] | list[dict[str, Any]],
|
|
476
504
|
) -> list[dict[str, Any]]:
|
|
@@ -478,6 +506,10 @@ def _normalize_options(
|
|
|
478
506
|
|
|
479
507
|
First item is marked as the default selection (checked=True) unless
|
|
480
508
|
any caller-supplied dict already sets it.
|
|
509
|
+
|
|
510
|
+
Option ``value``/``key`` must be plain text; a relation/object shape
|
|
511
|
+
or an unsupported field (e.g. ``type``) is rejected up front rather
|
|
512
|
+
than silently mangled or dropped.
|
|
481
513
|
"""
|
|
482
514
|
colors = [
|
|
483
515
|
"#C9E6FC", "#C3F2F2", "#C2F1D2", "#FFE7B1", "#FBD2BF",
|
|
@@ -487,6 +519,27 @@ def _normalize_options(
|
|
|
487
519
|
any_default = any(isinstance(o, dict) and o.get("checked") for o in opts)
|
|
488
520
|
for i, o in enumerate(opts):
|
|
489
521
|
if isinstance(o, dict):
|
|
522
|
+
unknown = set(o) - _ALLOWED_OPTION_FIELDS
|
|
523
|
+
if unknown:
|
|
524
|
+
raise ValueError(
|
|
525
|
+
f"Select option {o!r} has unsupported field(s) "
|
|
526
|
+
f"{sorted(unknown)}; an option only accepts "
|
|
527
|
+
f"{sorted(_ALLOWED_OPTION_FIELDS)}."
|
|
528
|
+
)
|
|
529
|
+
if o.get("value") in (None, ""):
|
|
530
|
+
raise ValueError(
|
|
531
|
+
f"Select option {o!r} is missing a non-empty 'value'."
|
|
532
|
+
)
|
|
533
|
+
if _option_value_is_object_shaped(o.get("value")):
|
|
534
|
+
raise ValueError(
|
|
535
|
+
f"Select option 'value' must be plain text, got a "
|
|
536
|
+
f"relation/object-shaped value: {o.get('value')!r}."
|
|
537
|
+
)
|
|
538
|
+
if o.get("key") is not None and _option_value_is_object_shaped(o.get("key")):
|
|
539
|
+
raise ValueError(
|
|
540
|
+
f"Select option 'key' must be a string, got a "
|
|
541
|
+
f"relation/object-shaped value: {o.get('key')!r}."
|
|
542
|
+
)
|
|
490
543
|
entry = {
|
|
491
544
|
"key": o.get("key") or str(uuid.uuid4()),
|
|
492
545
|
"value": o["value"],
|
|
@@ -495,6 +548,8 @@ def _normalize_options(
|
|
|
495
548
|
"checked": o.get("checked", False),
|
|
496
549
|
"color": o.get("color", colors[i % len(colors)]),
|
|
497
550
|
}
|
|
551
|
+
if "score" in o:
|
|
552
|
+
entry["score"] = o["score"]
|
|
498
553
|
else:
|
|
499
554
|
entry = {
|
|
500
555
|
"key": str(uuid.uuid4()),
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Member / department / org-role / relation cells must always serialize
|
|
2
|
+
to a JSON *array* of id objects, and must accept object-shaped inputs.
|
|
3
|
+
|
|
4
|
+
A USER_PICKER (26) cell is always an array, even for a single member
|
|
5
|
+
(evidence: a bare ``{"accountId":...}`` object on AddWorksheetRow fails
|
|
6
|
+
server-side). hap-cli's encoder already wraps scalars in an array, but it
|
|
7
|
+
mangled object-shaped inputs: ``[{"accountId": str(i)} for i in value]``
|
|
8
|
+
ran ``str()`` over a whole member dict when the value was a member object
|
|
9
|
+
or — the natural ``get_records`` read-back shape — a list of member
|
|
10
|
+
objects ``[{"accountId","fullname","avatar",...}]``. The id is now
|
|
11
|
+
extracted from such objects and the cell is always emitted as an array.
|
|
12
|
+
|
|
13
|
+
The same fix covers DEPARTMENT (27, ``departmentId``), ORG_ROLE
|
|
14
|
+
(48, ``organizeId``) and RELATE_SHEET/CASCADING (29/35, ``sid``).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
from hap_cli.core import record as rec_mod
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _val(type_code, value):
|
|
25
|
+
return rec_mod._legacy_cell_value({"type": type_code, "controlName": "f"}, value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestUserPickerAlwaysArray:
|
|
29
|
+
def test_single_account_id_string(self):
|
|
30
|
+
assert _val(26, "a1") == '[{"accountId": "a1"}]'
|
|
31
|
+
|
|
32
|
+
def test_comma_separated_ids(self):
|
|
33
|
+
assert _val(26, "a1,a2") == '[{"accountId": "a1"}, {"accountId": "a2"}]'
|
|
34
|
+
|
|
35
|
+
def test_list_of_ids(self):
|
|
36
|
+
assert _val(26, ["a1", "a2"]) == \
|
|
37
|
+
'[{"accountId": "a1"}, {"accountId": "a2"}]'
|
|
38
|
+
|
|
39
|
+
def test_single_member_object_keeps_only_account_id(self):
|
|
40
|
+
v = {"accountId": "a1", "fullname": "林梦阳", "name": "林梦阳",
|
|
41
|
+
"avatar": "https://x", "avatarMiddle": "https://y"}
|
|
42
|
+
assert _val(26, v) == '[{"accountId": "a1"}]'
|
|
43
|
+
|
|
44
|
+
def test_list_of_member_objects_readback_shape(self):
|
|
45
|
+
v = [{"accountId": "a1", "fullname": "甲", "avatar": "u1"},
|
|
46
|
+
{"accountId": "a2", "fullname": "乙", "avatar": "u2"}]
|
|
47
|
+
assert _val(26, v) == \
|
|
48
|
+
'[{"accountId": "a1"}, {"accountId": "a2"}]'
|
|
49
|
+
|
|
50
|
+
def test_json_array_of_objects_string(self):
|
|
51
|
+
v = json.dumps([{"accountId": "a1", "fullname": "甲"}])
|
|
52
|
+
assert _val(26, v) == '[{"accountId": "a1"}]'
|
|
53
|
+
|
|
54
|
+
def test_json_object_string(self):
|
|
55
|
+
v = json.dumps({"accountId": "a1", "fullname": "甲"})
|
|
56
|
+
assert _val(26, v) == '[{"accountId": "a1"}]'
|
|
57
|
+
|
|
58
|
+
def test_member_object_without_account_id_rejected(self):
|
|
59
|
+
with pytest.raises(ValueError):
|
|
60
|
+
_val(26, {"fullname": "甲", "avatar": "u"})
|
|
61
|
+
|
|
62
|
+
def test_empty_is_empty_array(self):
|
|
63
|
+
assert _val(26, "") == "[]"
|
|
64
|
+
assert _val(26, None) == "[]"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestDepartmentObjectShape:
|
|
68
|
+
def test_object_keeps_department_id(self):
|
|
69
|
+
v = {"departmentId": "d1", "departmentName": "研发"}
|
|
70
|
+
assert _val(27, v) == '[{"departmentId": "d1"}]'
|
|
71
|
+
|
|
72
|
+
def test_list_of_objects(self):
|
|
73
|
+
v = [{"departmentId": "d1"}, {"departmentId": "d2"}]
|
|
74
|
+
assert _val(27, v) == \
|
|
75
|
+
'[{"departmentId": "d1"}, {"departmentId": "d2"}]'
|
|
76
|
+
|
|
77
|
+
def test_plain_id_still_works(self):
|
|
78
|
+
assert _val(27, "d1") == '[{"departmentId": "d1"}]'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestOrgRoleObjectShape:
|
|
82
|
+
def test_object_keeps_organize_id(self):
|
|
83
|
+
v = {"organizeId": "o1", "organizeName": "主管"}
|
|
84
|
+
assert _val(48, v) == '[{"organizeId": "o1"}]'
|
|
85
|
+
|
|
86
|
+
def test_plain_id_still_works(self):
|
|
87
|
+
assert _val(48, "o1") == '[{"organizeId": "o1"}]'
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestRelateObjectShape:
|
|
91
|
+
def test_relate_object_keeps_sid(self):
|
|
92
|
+
v = {"sid": "row1", "name": "记录甲"}
|
|
93
|
+
assert _val(29, v) == '[{"sid": "row1"}]'
|
|
94
|
+
|
|
95
|
+
def test_relate_list_of_objects_readback(self):
|
|
96
|
+
v = [{"sid": "row1", "name": "甲"}, {"sid": "row2", "name": "乙"}]
|
|
97
|
+
assert _val(29, v) == '[{"sid": "row1"}, {"sid": "row2"}]'
|
|
98
|
+
|
|
99
|
+
def test_relate_plain_rowid_still_works(self):
|
|
100
|
+
assert _val(29, "row1") == '[{"sid": "row1"}]'
|
|
101
|
+
|
|
102
|
+
def test_cascading_object_keeps_sid(self):
|
|
103
|
+
assert _val(35, {"sid": "row1"}) == '[{"sid": "row1"}]'
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""A single/multi-select cell must reject relation/object-shaped values.
|
|
2
|
+
|
|
3
|
+
Traced from a live backend error on ``WorksheetService/AddWSRowsBatch``
|
|
4
|
+
(the batch service the main-site ``Worksheet/AddWorksheetRow`` dispatches
|
|
5
|
+
sub-table child rows to). A single-select child field (Type 11) had been
|
|
6
|
+
handed a RELATE_SHEET-shaped value ``[{"sid": "<rowId>"}]`` instead of an
|
|
7
|
+
option key, and the server failed deserialization:
|
|
8
|
+
|
|
9
|
+
Cannot deserialize the current JSON array (e.g. [1,2,3]) into type
|
|
10
|
+
'MD.Worksheet.Core.Entities.ControlOptions' ...
|
|
11
|
+
|
|
12
|
+
Before this fix, ``_resolve_option_keys`` passed any value it could not
|
|
13
|
+
map to a known key/label straight through to the server (the same soft
|
|
14
|
+
spot as bug #036430). A select cell legitimately carries only an option
|
|
15
|
+
key, a known label, or an ``add_<text>`` new-option directive (evidence:
|
|
16
|
+
pd-openweb src/components/Form/core/utils.js, formatControlToServer
|
|
17
|
+
case 10/11). An object / array-of-objects value is structurally a
|
|
18
|
+
relation/member/sub-table shape and is always wrong here, so it is now
|
|
19
|
+
rejected loudly at the CLI boundary instead of poisoning the server.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
from hap_cli.core import record as rec_mod
|
|
27
|
+
from hap_cli.core import worksheet_templates as wt
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_SELECT_CTRL = {
|
|
31
|
+
"controlId": "c_status",
|
|
32
|
+
"controlName": "状态",
|
|
33
|
+
"type": 11,
|
|
34
|
+
"options": [
|
|
35
|
+
{"key": "k_pass", "value": "已通过"},
|
|
36
|
+
{"key": "k_reject", "value": "已驳回"},
|
|
37
|
+
],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# A sub-table child whose option metadata is NOT carried on the child
|
|
41
|
+
# control (relationControls are often slim) — the structural rejection
|
|
42
|
+
# must still fire without an option set to validate against.
|
|
43
|
+
_SELECT_CTRL_NO_OPTS = {
|
|
44
|
+
"controlId": "cc_status",
|
|
45
|
+
"controlName": "子状态",
|
|
46
|
+
"type": 11,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_RELATION_VALUE = [{"sid": "3587620f-f900-4f9c-9803-43827e69ff21"}]
|
|
50
|
+
_RELATION_JSON = json.dumps(_RELATION_VALUE) # '[{"sid": "..."}]'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestResolveOptionKeysRejectsRelationShape:
|
|
54
|
+
def test_rejects_list_of_dicts(self):
|
|
55
|
+
with pytest.raises(ValueError, match="状态"):
|
|
56
|
+
rec_mod._resolve_option_keys(_SELECT_CTRL, _RELATION_VALUE)
|
|
57
|
+
|
|
58
|
+
def test_rejects_relation_json_string(self):
|
|
59
|
+
with pytest.raises(ValueError):
|
|
60
|
+
rec_mod._resolve_option_keys(_SELECT_CTRL, _RELATION_JSON)
|
|
61
|
+
|
|
62
|
+
def test_rejects_single_dict(self):
|
|
63
|
+
with pytest.raises(ValueError):
|
|
64
|
+
rec_mod._resolve_option_keys(
|
|
65
|
+
_SELECT_CTRL, {"sid": "3587620f-f900-4f9c-9803-43827e69ff21"})
|
|
66
|
+
|
|
67
|
+
def test_rejects_even_without_option_metadata(self):
|
|
68
|
+
# No options to validate against, but the shape is still wrong.
|
|
69
|
+
with pytest.raises(ValueError):
|
|
70
|
+
rec_mod._resolve_option_keys(_SELECT_CTRL_NO_OPTS, _RELATION_VALUE)
|
|
71
|
+
|
|
72
|
+
def test_rejects_json_object_string(self):
|
|
73
|
+
with pytest.raises(ValueError):
|
|
74
|
+
rec_mod._resolve_option_keys(_SELECT_CTRL, '{"sid": "abc"}')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestResolveOptionKeysStillAcceptsLegitValues:
|
|
78
|
+
def test_valid_key(self):
|
|
79
|
+
assert rec_mod._resolve_option_keys(_SELECT_CTRL, "k_pass") == ["k_pass"]
|
|
80
|
+
|
|
81
|
+
def test_label_maps_to_key(self):
|
|
82
|
+
assert rec_mod._resolve_option_keys(_SELECT_CTRL, "已通过") == ["k_pass"]
|
|
83
|
+
|
|
84
|
+
def test_add_new_option_directive_passes_through(self):
|
|
85
|
+
# `add_<text>` is how the form encodes "create this option on save".
|
|
86
|
+
assert rec_mod._resolve_option_keys(
|
|
87
|
+
_SELECT_CTRL, "add_新选项") == ["add_新选项"]
|
|
88
|
+
|
|
89
|
+
def test_unknown_plain_text_passes_through(self):
|
|
90
|
+
# Plain scalar text is not an object shape — left to the server
|
|
91
|
+
# (may be a key we cannot see, or a new option). Not our bug.
|
|
92
|
+
assert rec_mod._resolve_option_keys(
|
|
93
|
+
_SELECT_CTRL_NO_OPTS, "甲") == ["甲"]
|
|
94
|
+
|
|
95
|
+
def test_comma_list_of_keys(self):
|
|
96
|
+
assert rec_mod._resolve_option_keys(
|
|
97
|
+
_SELECT_CTRL, "k_pass,k_reject") == ["k_pass", "k_reject"]
|
|
98
|
+
|
|
99
|
+
def test_empty_is_cleared(self):
|
|
100
|
+
assert rec_mod._resolve_option_keys(_SELECT_CTRL, "") == []
|
|
101
|
+
assert rec_mod._resolve_option_keys(_SELECT_CTRL, None) == []
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestSubTableChildSelectRejectsRelationValue:
|
|
105
|
+
"""The reported scenario: a sub-table (34) child single-select gets a
|
|
106
|
+
relation-shaped value while the parent record is created — the child
|
|
107
|
+
row is what the backend batch-saves."""
|
|
108
|
+
|
|
109
|
+
def test_sublist_child_select_relation_value_is_rejected(self):
|
|
110
|
+
sublist_ctrl = {
|
|
111
|
+
"controlId": "c_sub",
|
|
112
|
+
"controlName": "明细",
|
|
113
|
+
"type": 34,
|
|
114
|
+
"relationControls": [_SELECT_CTRL_NO_OPTS],
|
|
115
|
+
}
|
|
116
|
+
rows = [{"子状态": _RELATION_VALUE}]
|
|
117
|
+
with pytest.raises(ValueError):
|
|
118
|
+
rec_mod._legacy_cell_value(sublist_ctrl, rows)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestOptionDefinitionRejectsBadShapes:
|
|
122
|
+
"""The other injection point: defining a select field's *options*.
|
|
123
|
+
|
|
124
|
+
A select option is only ``{key, value, isDeleted, index, checked,
|
|
125
|
+
color}`` (evidence: pd-openweb widgetConfig/util/setting.js). Option
|
|
126
|
+
``value`` / ``key`` must be plain text — never a relation/object shape
|
|
127
|
+
— and an option carries no ``type`` field. Before this fix,
|
|
128
|
+
``_normalize_options`` passed any value/key straight through and
|
|
129
|
+
silently dropped unknown keys like ``type``.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def test_option_value_object_shape_rejected(self):
|
|
133
|
+
with pytest.raises(ValueError):
|
|
134
|
+
wt.build_control("DROP_DOWN", "状态",
|
|
135
|
+
options=[{"value": [{"sid": "abc"}]}])
|
|
136
|
+
|
|
137
|
+
def test_option_value_relation_json_string_rejected(self):
|
|
138
|
+
with pytest.raises(ValueError):
|
|
139
|
+
wt.build_control("DROP_DOWN", "状态",
|
|
140
|
+
options=[{"value": _RELATION_JSON}])
|
|
141
|
+
|
|
142
|
+
def test_option_key_object_shape_rejected(self):
|
|
143
|
+
with pytest.raises(ValueError):
|
|
144
|
+
wt.build_control("DROP_DOWN", "状态",
|
|
145
|
+
options=[{"key": [{"sid": "abc"}], "value": "甲"}])
|
|
146
|
+
|
|
147
|
+
def test_option_unknown_type_field_rejected(self):
|
|
148
|
+
with pytest.raises(ValueError, match="type"):
|
|
149
|
+
wt.build_control("DROP_DOWN", "状态",
|
|
150
|
+
options=[{"value": "甲", "type": 0}])
|
|
151
|
+
|
|
152
|
+
def test_option_missing_value_rejected(self):
|
|
153
|
+
with pytest.raises(ValueError):
|
|
154
|
+
wt.build_control("DROP_DOWN", "状态", options=[{"key": "k1"}])
|
|
155
|
+
|
|
156
|
+
# --- legit option definitions still build ---
|
|
157
|
+
|
|
158
|
+
def test_string_options_ok(self):
|
|
159
|
+
out = wt.build_control("DROP_DOWN", "状态", options=["甲", "乙"])
|
|
160
|
+
assert [o["value"] for o in out["options"]] == ["甲", "乙"]
|
|
161
|
+
|
|
162
|
+
def test_dict_option_with_known_fields_ok(self):
|
|
163
|
+
out = wt.build_control(
|
|
164
|
+
"DROP_DOWN", "状态",
|
|
165
|
+
options=[{"value": "甲", "key": "k1", "checked": True}])
|
|
166
|
+
assert out["options"][0]["key"] == "k1"
|
|
167
|
+
assert out["options"][0]["value"] == "甲"
|
|
168
|
+
assert out["options"][0]["checked"] is True
|
|
169
|
+
|
|
170
|
+
def test_scored_option_is_allowed_and_preserved(self):
|
|
171
|
+
# `score` is a legitimate NPS/scoring field — it must not be
|
|
172
|
+
# rejected (it is not a relation shape) and must survive to the
|
|
173
|
+
# wire instead of being silently dropped.
|
|
174
|
+
out = wt.build_control(
|
|
175
|
+
"DROP_DOWN", "满意度",
|
|
176
|
+
options=[{"value": "满意", "score": 5}])
|
|
177
|
+
assert out["options"][0]["value"] == "满意"
|
|
178
|
+
assert out["options"][0]["score"] == 5
|