hap-cli 0.8.28__tar.gz → 0.8.30__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.28 → hap_cli-0.8.30}/PKG-INFO +1 -1
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/__init__.py +1 -1
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/worksheet_cmd.py +122 -3
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/executor.py +6 -12
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/upload.py +25 -1
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/worksheet.py +229 -6
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/locale/messages.json +24 -4
- hap_cli-0.8.30/hap_cli/tests/test_upload_allow_down.py +118 -0
- hap_cli-0.8.30/hap_cli/tests/test_worksheet_switch.py +174 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/PKG-INFO +1 -1
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/SOURCES.txt +2 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/MANIFEST.in +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/README.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/__main__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/config.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/harness.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/smoke.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/state.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/README.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/README_CN.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/__main__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/app_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/app_creator_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/app_editor_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/attachment_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/auth_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/calendar_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/chart_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/chat_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/chatbot_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/config_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/contact_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/department_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/group_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/guide_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/icon_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/instance_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/node_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/optionset_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/page_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/post_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/record_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/region_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/role_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/update_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/upload_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/v3_registry.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/workflow_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/context.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/action_spec_adapter.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/charts.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/cleanup.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/compiler.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/config.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/design/design.schema.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/errors.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/executor.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/fields.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/hap.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/merge.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/recording/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/recording/console.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/recording/mirror.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/recording/report.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/schema.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/template.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/selftest.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/smoke.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/steps.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/store.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/validate.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/attachment.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/auth.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/calendar_mod.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/chart.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/chat.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/chatbot.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/cli_access.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/contact.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/control_type_codes.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/department.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/apply.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/componentlower.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/editspec/component.schema.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/editspec/custom-action.schema.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/editspec/envelope.schema.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/editspec/field.schema.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/editspec.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/errors.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/fieldlower.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/jsonschema_mini.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/models.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/ops.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/planner.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/reader.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/field_normalizer.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/field_spec_adapter.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/filter_translator.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/flow_node.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/global_meta.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/group.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/guide.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/icon_index.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/instance.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/logger.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/optionset.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/page.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/post.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/record.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/response_crypto.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/role.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/role_perm_builder.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/session.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/token_crypto.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/update.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/v3/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/v3/dispatcher.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/v3/render.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/v3/schema.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/view_spec_adapter.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/workflow.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/workflow_node_dsl.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/worksheet_templates.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/approval.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/chart.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/messages.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/overview.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/post.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/record.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/setup.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/skills.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/index.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/approval.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/chart.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/messages.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/overview.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/post.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/record.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/setup.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/skills.md +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/hap_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/i18n.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/locale/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/locale/messages.schema.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/conftest.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_app_creator.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_app_editor.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_app_export_usage.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_approval_submit_fill_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_attachment_share.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_auth_prerelease.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_bug_036430.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_bug_036750.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_chart.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_chat_attachment_send.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_chat_recall_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_cli_access.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_config_cmd.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_content_language.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_control_lookup_guard.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_core.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_full_e2e.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_global_meta.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_guide.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_i18n.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_instance_history_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_approval.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_approval_actions.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_calendar.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_destructive.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_misc.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_post.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_social.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_v3.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_workflow.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_member_relation_array_wrap.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_multi_profile.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_no_workflow_unsupported.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_org_id_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_org_id_docs.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_parameter_conventions.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_post_scope_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_record_field_alias.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_record_write_fixes.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_select_add_option.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_select_rejects_relation_value.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_semantic_types_registry.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_update.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_update_mode.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_upload_host_resolution.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_v3_dispatcher.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_v3_session.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_workflow_envelope_errors.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_workflow_list_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_workflow_read_write_keys.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_worksheet_create_section.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/utils/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/utils/formatting.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/utils/options.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/utils/parameter_mapping.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/dependency_links.txt +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/entry_points.txt +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/requires.txt +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/top_level.txt +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/__main__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/_wftest.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/charts.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/cleanup.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/compiler.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/config.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/errors.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/executor.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/fields.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/hap.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/recording/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/recording/console.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/recording/jsonl.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/recording/mirror.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/recording/report.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/schema.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/seed/__init__.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/seed/cli.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/seed/executor.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/seed/template.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/selftest.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/smoke.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/step.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/steps.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/store.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/workflow_dsl.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/setup.cfg +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/setup.py +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/INDEX.json +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/create_optionset.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/create_role.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/delete_optionset.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/delete_record.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/delete_role.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_app_info.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_details.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_list.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_logs.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_relations.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_regions.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_role_details.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_role_list.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/knowledge_search.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
- {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/update_optionset.yaml +0 -0
|
@@ -919,24 +919,143 @@ def worksheet_delete_button(ctx, worksheet_id, btn_id, app_id, view_id, yes):
|
|
|
919
919
|
|
|
920
920
|
@worksheet.command("switches")
|
|
921
921
|
@click.argument("worksheet_id")
|
|
922
|
+
@click.option("--raw", is_flag=True,
|
|
923
|
+
help="Return the server's switch list unannotated.")
|
|
922
924
|
@pass_context
|
|
923
|
-
def worksheet_switches(ctx, worksheet_id):
|
|
924
|
-
"""Get feature switches for a worksheet.
|
|
925
|
+
def worksheet_switches(ctx, worksheet_id, raw):
|
|
926
|
+
"""Get feature switches for a worksheet.
|
|
927
|
+
|
|
928
|
+
Each switch is listed with the name and group it has on the feature
|
|
929
|
+
switch page, because several numbers share a name and only the group
|
|
930
|
+
tells them apart.
|
|
931
|
+
"""
|
|
925
932
|
try:
|
|
926
933
|
session = ctx.get_session()
|
|
927
934
|
data = ws_mod.get_switches(session, worksheet_id)
|
|
935
|
+
if not raw:
|
|
936
|
+
data = ws_mod.annotate_switches(data)
|
|
928
937
|
ctx.output(data, lambda d: output_json(d))
|
|
929
938
|
except Exception as e:
|
|
930
939
|
ctx.handle_error(e)
|
|
931
940
|
|
|
932
941
|
|
|
942
|
+
@worksheet.command("switch-types")
|
|
943
|
+
@pass_context
|
|
944
|
+
def worksheet_switch_types(ctx):
|
|
945
|
+
"""Show the feature switch numbers with their names and groups."""
|
|
946
|
+
rows = [ws_mod.describe_switch(t) for t in sorted(ws_mod.SWITCH_TYPES)]
|
|
947
|
+
ctx.output(
|
|
948
|
+
rows,
|
|
949
|
+
lambda d: output_table(
|
|
950
|
+
d, ["type", "group", "name", "path"],
|
|
951
|
+
["Type", "Group", "Name", "Path"],
|
|
952
|
+
),
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _parse_switch_changes(pairs):
|
|
957
|
+
changes = {}
|
|
958
|
+
for raw in pairs:
|
|
959
|
+
if "=" not in raw:
|
|
960
|
+
raise click.UsageError(
|
|
961
|
+
f"Invalid --set value {raw!r}. Use --set TYPE=true|false.")
|
|
962
|
+
key, value = raw.split("=", 1)
|
|
963
|
+
try:
|
|
964
|
+
stype = int(key.strip())
|
|
965
|
+
except ValueError:
|
|
966
|
+
raise click.UsageError(
|
|
967
|
+
f"Invalid switch number {key!r}. Run `hap worksheet "
|
|
968
|
+
"switch-types` for the list.")
|
|
969
|
+
val = value.strip().lower()
|
|
970
|
+
if val not in ("true", "false", "1", "0", "on", "off"):
|
|
971
|
+
raise click.UsageError(
|
|
972
|
+
f"Invalid state {value!r} for switch {stype}. Use true or false.")
|
|
973
|
+
changes[stype] = val in ("true", "1", "on")
|
|
974
|
+
return changes
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
@worksheet.command("batch-edit-switch")
|
|
978
|
+
@click.argument("worksheet_ids", nargs=-1, required=True)
|
|
979
|
+
@click.option("--set", "pairs", multiple=True, required=True,
|
|
980
|
+
help="Switch to change as TYPE=true|false (repeatable). "
|
|
981
|
+
"Numbers come from `hap worksheet switch-types`.")
|
|
982
|
+
@click.option("--dry-run", is_flag=True,
|
|
983
|
+
help="Show what would change on each worksheet, without saving.")
|
|
984
|
+
@pass_context
|
|
985
|
+
def worksheet_batch_edit_switch(ctx, worksheet_ids, pairs, dry_run):
|
|
986
|
+
"""Change several feature switches on one or more worksheets.
|
|
987
|
+
|
|
988
|
+
The switches you do not name keep their current values. Turning the
|
|
989
|
+
批量操作 or 统计 parent on or off carries its children with it. Each
|
|
990
|
+
worksheet is read back after saving, and any switch that did not take
|
|
991
|
+
is reported rather than counted as success.
|
|
992
|
+
"""
|
|
993
|
+
try:
|
|
994
|
+
changes = _parse_switch_changes(pairs)
|
|
995
|
+
session = ctx.get_session()
|
|
996
|
+
if dry_run:
|
|
997
|
+
preview = [
|
|
998
|
+
{**ws_mod.describe_switch(t), "state": v}
|
|
999
|
+
for t, v in changes.items()
|
|
1000
|
+
]
|
|
1001
|
+
ctx.output(
|
|
1002
|
+
{"worksheetIds": list(worksheet_ids), "changes": preview,
|
|
1003
|
+
"saved": False},
|
|
1004
|
+
lambda d: click.echo(
|
|
1005
|
+
f"Would set {len(preview)} switch(es) on "
|
|
1006
|
+
f"{len(worksheet_ids)} worksheet(s): "
|
|
1007
|
+
+ ", ".join(f"{c['path'] or c['type']}={c['state']}"
|
|
1008
|
+
for c in preview)),
|
|
1009
|
+
)
|
|
1010
|
+
return
|
|
1011
|
+
results, failed_sheets = [], []
|
|
1012
|
+
for wid in worksheet_ids:
|
|
1013
|
+
try:
|
|
1014
|
+
res = ws_mod.batch_edit_switch(session, wid, changes)
|
|
1015
|
+
except Exception as exc: # keep going across worksheets
|
|
1016
|
+
results.append({"worksheetId": wid, "error": str(exc)})
|
|
1017
|
+
failed_sheets.append(wid)
|
|
1018
|
+
continue
|
|
1019
|
+
results.append(res)
|
|
1020
|
+
if res.get("failed"):
|
|
1021
|
+
failed_sheets.append(wid)
|
|
1022
|
+
payload = {"results": results,
|
|
1023
|
+
"worksheetsChanged": len(worksheet_ids) - len(failed_sheets),
|
|
1024
|
+
"worksheetsFailed": failed_sheets}
|
|
1025
|
+
|
|
1026
|
+
def _render(d):
|
|
1027
|
+
click.echo(f"Changed {d['worksheetsChanged']} of "
|
|
1028
|
+
f"{len(worksheet_ids)} worksheet(s).")
|
|
1029
|
+
for r in d["results"]:
|
|
1030
|
+
if r.get("error"):
|
|
1031
|
+
click.echo(f" {r['worksheetId']}: {r['error']}")
|
|
1032
|
+
elif r.get("failed"):
|
|
1033
|
+
for t, info in r["failed"].items():
|
|
1034
|
+
click.echo(f" {r['worksheetId']}: switch {t} is "
|
|
1035
|
+
f"{info['actual']!r}, wanted "
|
|
1036
|
+
f"{info['expected']!r}")
|
|
1037
|
+
ctx.output(payload, _render)
|
|
1038
|
+
if failed_sheets:
|
|
1039
|
+
raise click.ClickException(
|
|
1040
|
+
f"{len(failed_sheets)} worksheet(s) did not end up in the "
|
|
1041
|
+
"requested state: " + ", ".join(failed_sheets))
|
|
1042
|
+
except Exception as e:
|
|
1043
|
+
ctx.handle_error(e)
|
|
1044
|
+
|
|
1045
|
+
|
|
933
1046
|
@worksheet.command("edit-switch")
|
|
934
1047
|
@click.argument("worksheet_id")
|
|
935
1048
|
@click.argument("switch_id", type=int)
|
|
936
1049
|
@click.argument("value", type=bool)
|
|
937
1050
|
@pass_context
|
|
938
1051
|
def worksheet_edit_switch(ctx, worksheet_id, switch_id, value):
|
|
939
|
-
"""
|
|
1052
|
+
"""Turn one feature switch on or off.
|
|
1053
|
+
|
|
1054
|
+
SWITCH_ID is a switch number from `hap worksheet switch-types`. The
|
|
1055
|
+
new state is read back, so a change that did not take is reported as
|
|
1056
|
+
an error instead of success. To change several switches, or the same
|
|
1057
|
+
ones across many worksheets, use `worksheet batch-edit-switch`.
|
|
1058
|
+
"""
|
|
940
1059
|
try:
|
|
941
1060
|
session = ctx.get_session()
|
|
942
1061
|
data = ws_mod.edit_switch(session, worksheet_id, switch_id, value)
|
|
@@ -141,18 +141,12 @@ class _Resolver:
|
|
|
141
141
|
|
|
142
142
|
def _record_attachment_cell(descriptors: list[dict[str, Any]]) -> dict[str, Any]:
|
|
143
143
|
"""Wrap general uploaded-file descriptors into a worksheet Attachment
|
|
144
|
-
(type 14) cell value
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"fileName": d.get("fileName"), "fileExt": d.get("fileExt"),
|
|
151
|
-
"originalFileName": d.get("originalFileName"), "key": d.get("key"),
|
|
152
|
-
"url": d.get("url"), "oldOriginalFileName": d.get("originalFileName"),
|
|
153
|
-
"index": i, "isEdit": False,
|
|
154
|
-
})
|
|
155
|
-
return {"attachmentData": [], "attachments": attachments, "knowledgeAtts": []}
|
|
144
|
+
(type 14) cell value. Delegates to the core wrapper so the seeded rows
|
|
145
|
+
carry exactly what any other CLI upload does — the local copy this
|
|
146
|
+
replaced silently dropped each file's download permission."""
|
|
147
|
+
from hap_cli.core.upload import record_attachment_cell
|
|
148
|
+
|
|
149
|
+
return record_attachment_cell(descriptors)
|
|
156
150
|
|
|
157
151
|
|
|
158
152
|
def _upload_row_attachments(
|
|
@@ -209,6 +209,23 @@ def _iso(deadline: Optional[int]) -> Optional[str]:
|
|
|
209
209
|
return datetime.fromtimestamp(deadline, tz=timezone.utc).astimezone().isoformat()
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
def _download_permission(ext: str) -> dict[str, Any]:
|
|
213
|
+
"""The per-file download permission the holder of the file is sent.
|
|
214
|
+
|
|
215
|
+
Whoever stores an attachment decides, per file, whether anyone but
|
|
216
|
+
the uploader may download it — an omitted flag used to read as "no",
|
|
217
|
+
so a file attached by the CLI answered 权限不足 for every colleague.
|
|
218
|
+
pd-openweb sets it in ``formatResponseData``
|
|
219
|
+
(src/components/UploadFiles/utils.js:202-206): non-pictures get
|
|
220
|
+
``allowDown: true`` plus an empty ``docVersionID``, pictures get
|
|
221
|
+
neither because the viewer already treats a picture as downloadable
|
|
222
|
+
(UploadFiles/File.js:724). Mirrored verbatim, empty dict included.
|
|
223
|
+
"""
|
|
224
|
+
if ext.lower() in _PICTURE_EXTS:
|
|
225
|
+
return {}
|
|
226
|
+
return {"allowDown": True, "docVersionID": ""}
|
|
227
|
+
|
|
228
|
+
|
|
212
229
|
def upload_bytes(
|
|
213
230
|
session: Session,
|
|
214
231
|
*,
|
|
@@ -239,7 +256,7 @@ def upload_bytes(
|
|
|
239
256
|
up = _post_to_store(upload_host(session), token, content,
|
|
240
257
|
original_name=original, ext=ext,
|
|
241
258
|
server_name=server_name)
|
|
242
|
-
|
|
259
|
+
descriptor = {
|
|
243
260
|
"fileID": generate_file_oid(),
|
|
244
261
|
"fileSize": len(content),
|
|
245
262
|
"etag": up.get("etag"),
|
|
@@ -256,6 +273,8 @@ def upload_bytes(
|
|
|
256
273
|
# (public-bucket) address, which does not expire.
|
|
257
274
|
"urlExpiresAt": _iso(url_expires_at(token.get("url"))),
|
|
258
275
|
}
|
|
276
|
+
descriptor.update(_download_permission(ext))
|
|
277
|
+
return descriptor
|
|
259
278
|
|
|
260
279
|
|
|
261
280
|
def source_descriptors(sources: "list[str] | tuple[str, ...]") -> list[dict[str, str]]:
|
|
@@ -324,4 +343,9 @@ def record_attachment_cell(descriptors: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
324
343
|
"index": i,
|
|
325
344
|
"isEdit": False,
|
|
326
345
|
})
|
|
346
|
+
# Carried, not recomputed: the descriptor already decided this at
|
|
347
|
+
# upload time — see _download_permission().
|
|
348
|
+
for key in ("allowDown", "docVersionID"):
|
|
349
|
+
if key in d:
|
|
350
|
+
attachments[-1][key] = d[key]
|
|
327
351
|
return {"attachmentData": [], "attachments": attachments, "knowledgeAtts": []}
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
from typing import Any, Optional
|
|
6
6
|
|
|
7
|
-
from hap_cli.core.session import Session
|
|
7
|
+
from hap_cli.core.session import Session, APIError
|
|
8
8
|
from hap_cli.core import worksheet_templates as wt
|
|
9
9
|
|
|
10
10
|
logger = logging.getLogger("hap_cli.worksheet")
|
|
@@ -1681,14 +1681,237 @@ def get_switches(
|
|
|
1681
1681
|
)
|
|
1682
1682
|
|
|
1683
1683
|
|
|
1684
|
+
# ── Feature switch catalog ───────────────────────────────────────────────
|
|
1685
|
+
#
|
|
1686
|
+
# GetSwitch answers bare numbers, which say nothing about what they
|
|
1687
|
+
# control, and several numbers share a label: 26 and 36 are both "复制",
|
|
1688
|
+
# 28 and 39 are both "删除", 23 and 32 are both "系统默认打印". Only the
|
|
1689
|
+
# group tells them apart, so the group travels with the name.
|
|
1690
|
+
#
|
|
1691
|
+
# Names, grouping and parent/child structure come from pd-openweb
|
|
1692
|
+
# ``src/pages/FormSet/containers/FunctionalSwitch/config.js``
|
|
1693
|
+
# (``listConfigStr``, ``worksheetSwitch`` / ``viewSwitch`` /
|
|
1694
|
+
# ``recordSwitch`` / ``approveSwitch``, ``batch``, ``statistics``).
|
|
1695
|
+
|
|
1696
|
+
SWITCH_PARENT_BATCH = 25 # 视图 > 批量操作
|
|
1697
|
+
SWITCH_PARENT_STATISTICS = 500 # 工作表 > 统计
|
|
1698
|
+
|
|
1699
|
+
_SWITCH_BATCH_CHILDREN = (24, 26, 23, 27, 28, 29)
|
|
1700
|
+
_SWITCH_STATISTICS_CHILDREN = (50, 51)
|
|
1701
|
+
|
|
1702
|
+
SWITCH_TYPES: dict[int, dict[str, Any]] = {
|
|
1703
|
+
# 工作表
|
|
1704
|
+
10: {"name": "显示创建按钮", "group": "工作表"},
|
|
1705
|
+
11: {"name": "讨论", "group": "工作表"},
|
|
1706
|
+
12: {"name": "日志", "group": "工作表"},
|
|
1707
|
+
13: {"name": "导入", "group": "工作表"},
|
|
1708
|
+
14: {"name": "回收站", "group": "工作表"},
|
|
1709
|
+
500: {"name": "统计", "group": "工作表"},
|
|
1710
|
+
50: {"name": "公共", "group": "工作表", "parent": SWITCH_PARENT_STATISTICS},
|
|
1711
|
+
51: {"name": "个人", "group": "工作表", "parent": SWITCH_PARENT_STATISTICS},
|
|
1712
|
+
# 视图
|
|
1713
|
+
20: {"name": "公开分享", "group": "视图"},
|
|
1714
|
+
38: {"name": "嵌入链接", "group": "视图"},
|
|
1715
|
+
21: {"name": "导出", "group": "视图"},
|
|
1716
|
+
22: {"name": "快捷编辑", "group": "视图"},
|
|
1717
|
+
25: {"name": "批量操作", "group": "视图"},
|
|
1718
|
+
24: {"name": "编辑", "group": "视图", "parent": SWITCH_PARENT_BATCH},
|
|
1719
|
+
26: {"name": "复制", "group": "视图", "parent": SWITCH_PARENT_BATCH},
|
|
1720
|
+
23: {"name": "系统默认打印", "group": "视图", "parent": SWITCH_PARENT_BATCH},
|
|
1721
|
+
27: {"name": "导出", "group": "视图", "parent": SWITCH_PARENT_BATCH},
|
|
1722
|
+
28: {"name": "删除", "group": "视图", "parent": SWITCH_PARENT_BATCH},
|
|
1723
|
+
29: {"name": "执行自定义动作", "group": "视图", "parent": SWITCH_PARENT_BATCH},
|
|
1724
|
+
# 记录
|
|
1725
|
+
30: {"name": "公开分享", "group": "记录"},
|
|
1726
|
+
52: {"name": "内部访问链接", "group": "记录"},
|
|
1727
|
+
32: {"name": "系统默认打印", "group": "记录"},
|
|
1728
|
+
33: {"name": "附件下载", "group": "记录"},
|
|
1729
|
+
34: {"name": "日志", "group": "记录"},
|
|
1730
|
+
35: {"name": "讨论", "group": "记录"},
|
|
1731
|
+
36: {"name": "复制", "group": "记录"},
|
|
1732
|
+
39: {"name": "删除", "group": "记录"},
|
|
1733
|
+
37: {"name": "重新创建", "group": "记录"},
|
|
1734
|
+
# 审批
|
|
1735
|
+
40: {"name": "系统字段", "group": "审批"},
|
|
1736
|
+
41: {"name": "审批流转详情", "group": "审批"},
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
def describe_switch(switch_type: int) -> dict[str, Any]:
|
|
1741
|
+
"""Name, group and parent of a switch number; empty-ish when unknown."""
|
|
1742
|
+
meta = SWITCH_TYPES.get(switch_type)
|
|
1743
|
+
if not meta:
|
|
1744
|
+
return {"type": switch_type, "name": "", "group": "", "path": ""}
|
|
1745
|
+
parent = meta.get("parent")
|
|
1746
|
+
path = " > ".join(
|
|
1747
|
+
[meta["group"]]
|
|
1748
|
+
+ ([SWITCH_TYPES[parent]["name"]] if parent in SWITCH_TYPES else [])
|
|
1749
|
+
+ [meta["name"]]
|
|
1750
|
+
)
|
|
1751
|
+
out = {"type": switch_type, "name": meta["name"], "group": meta["group"],
|
|
1752
|
+
"path": path}
|
|
1753
|
+
if parent:
|
|
1754
|
+
out["parent"] = parent
|
|
1755
|
+
return out
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def annotate_switches(switches: Any) -> list[dict[str, Any]]:
|
|
1759
|
+
"""Return the switch list with name/group/path added to each entry."""
|
|
1760
|
+
out = []
|
|
1761
|
+
for entry in switches or []:
|
|
1762
|
+
if not isinstance(entry, dict):
|
|
1763
|
+
continue
|
|
1764
|
+
merged = dict(entry)
|
|
1765
|
+
merged.update(describe_switch(entry.get("type")))
|
|
1766
|
+
out.append(merged)
|
|
1767
|
+
return out
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
def _switch_wire_fields(entry: dict[str, Any]) -> dict[str, Any]:
|
|
1771
|
+
"""The subset of a switch the page sends back on save.
|
|
1772
|
+
|
|
1773
|
+
``FunctionalSwitch/index.jsx`` picks ``roleType/type/state/viewIds``;
|
|
1774
|
+
``displayFlowChart`` rides along where the switch carries one.
|
|
1775
|
+
"""
|
|
1776
|
+
out = {k: entry[k] for k in ("roleType", "type", "state", "viewIds")
|
|
1777
|
+
if k in entry}
|
|
1778
|
+
if "displayFlowChart" in entry:
|
|
1779
|
+
out["displayFlowChart"] = entry["displayFlowChart"]
|
|
1780
|
+
out.setdefault("type", entry.get("type"))
|
|
1781
|
+
out.setdefault("roleType", 0)
|
|
1782
|
+
return out
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
def _apply_switch_linkage(
|
|
1786
|
+
current: list[dict[str, Any]], changes: dict[int, bool],
|
|
1787
|
+
) -> dict[int, bool]:
|
|
1788
|
+
"""Expand ``changes`` with the parent/child rules the page applies.
|
|
1789
|
+
|
|
1790
|
+
* Turning the 批量操作 (25) or 统计 (500) parent on or off carries all
|
|
1791
|
+
of its children with it.
|
|
1792
|
+
* When every 统计 child ends up off, the parent goes off too. The
|
|
1793
|
+
批量操作 children deliberately do **not** close their parent — the
|
|
1794
|
+
page only does this for 统计 (``index.jsx:108-123``; its comment
|
|
1795
|
+
says 批量操作 but the code targets the statistics parent).
|
|
1796
|
+
"""
|
|
1797
|
+
resolved = dict(changes)
|
|
1798
|
+
for parent, children in (
|
|
1799
|
+
(SWITCH_PARENT_BATCH, _SWITCH_BATCH_CHILDREN),
|
|
1800
|
+
(SWITCH_PARENT_STATISTICS, _SWITCH_STATISTICS_CHILDREN),
|
|
1801
|
+
):
|
|
1802
|
+
if parent in resolved:
|
|
1803
|
+
for child in children:
|
|
1804
|
+
resolved.setdefault(child, resolved[parent])
|
|
1805
|
+
states = {e.get("type"): e.get("state") for e in current
|
|
1806
|
+
if isinstance(e, dict)}
|
|
1807
|
+
states.update(resolved)
|
|
1808
|
+
if any(c in resolved for c in _SWITCH_STATISTICS_CHILDREN):
|
|
1809
|
+
if not any(states.get(c) for c in _SWITCH_STATISTICS_CHILDREN):
|
|
1810
|
+
resolved.setdefault(SWITCH_PARENT_STATISTICS, False)
|
|
1811
|
+
return resolved
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
def batch_edit_switch(
|
|
1815
|
+
session: Session,
|
|
1816
|
+
worksheet_id: str,
|
|
1817
|
+
changes: dict[int, bool],
|
|
1818
|
+
*,
|
|
1819
|
+
verify: bool = True,
|
|
1820
|
+
) -> dict[str, Any]:
|
|
1821
|
+
"""Apply several switch changes to one worksheet in a single call.
|
|
1822
|
+
|
|
1823
|
+
This is what the feature-switch page itself uses
|
|
1824
|
+
(``Worksheet/BatchEditSwitch``, ``src/api/worksheet.js:2346-2356``):
|
|
1825
|
+
it submits the **whole** ``switchList``, so the untouched switches
|
|
1826
|
+
have to be read first and sent back unchanged or they are lost.
|
|
1827
|
+
|
|
1828
|
+
Parent/child linkage is applied for you (see
|
|
1829
|
+
:func:`_apply_switch_linkage`). With ``verify`` the new state is read
|
|
1830
|
+
back and any switch that did not take is reported, because the
|
|
1831
|
+
endpoint answers ``true`` regardless.
|
|
1832
|
+
|
|
1833
|
+
Returns ``{"worksheetId", "applied": {type: bool}, "failed": {type:
|
|
1834
|
+
{"expected", "actual"}}}``.
|
|
1835
|
+
"""
|
|
1836
|
+
current = get_switches(session, worksheet_id) or []
|
|
1837
|
+
if not isinstance(current, list):
|
|
1838
|
+
raise APIError(
|
|
1839
|
+
f"could not read the switches of worksheet {worksheet_id}")
|
|
1840
|
+
resolved = _apply_switch_linkage(current, {int(k): bool(v)
|
|
1841
|
+
for k, v in changes.items()})
|
|
1842
|
+
switch_list = []
|
|
1843
|
+
for entry in current:
|
|
1844
|
+
if not isinstance(entry, dict):
|
|
1845
|
+
continue
|
|
1846
|
+
wire = _switch_wire_fields(entry)
|
|
1847
|
+
if entry.get("type") in resolved:
|
|
1848
|
+
wire["state"] = resolved[entry["type"]]
|
|
1849
|
+
switch_list.append(wire)
|
|
1850
|
+
known = {e.get("type") for e in current if isinstance(e, dict)}
|
|
1851
|
+
for stype, state in resolved.items():
|
|
1852
|
+
if stype not in known:
|
|
1853
|
+
switch_list.append({"type": stype, "state": state, "roleType": 0})
|
|
1854
|
+
session.api_call(
|
|
1855
|
+
"Worksheet", "BatchEditSwitch",
|
|
1856
|
+
{"worksheetId": worksheet_id, "switchList": switch_list},
|
|
1857
|
+
)
|
|
1858
|
+
result: dict[str, Any] = {"worksheetId": worksheet_id,
|
|
1859
|
+
"applied": dict(resolved), "failed": {}}
|
|
1860
|
+
if verify:
|
|
1861
|
+
after = {e.get("type"): e.get("state")
|
|
1862
|
+
for e in (get_switches(session, worksheet_id) or [])
|
|
1863
|
+
if isinstance(e, dict)}
|
|
1864
|
+
for stype, want in resolved.items():
|
|
1865
|
+
if bool(after.get(stype)) != bool(want):
|
|
1866
|
+
result["failed"][stype] = {"expected": want,
|
|
1867
|
+
"actual": after.get(stype)}
|
|
1868
|
+
return result
|
|
1869
|
+
|
|
1870
|
+
|
|
1684
1871
|
def edit_switch(
|
|
1685
1872
|
session: Session, worksheet_id: str, switch_id: int, value: bool,
|
|
1686
1873
|
) -> dict[str, Any]:
|
|
1687
|
-
"""
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1874
|
+
"""Turn one feature switch on or off, and confirm it stuck.
|
|
1875
|
+
|
|
1876
|
+
``EditSwitch`` names the boolean ``state`` and the switch number
|
|
1877
|
+
``type`` (pd-openweb ``src/api/worksheet.js:2330-2343``). This wrapper
|
|
1878
|
+
used to send ``{state: <switch number>, value: <bool>}`` — the two
|
|
1879
|
+
swapped, plus a ``value`` field the API does not define. The server
|
|
1880
|
+
accepts that quietly and answers ``{"data": true}`` without changing
|
|
1881
|
+
anything, so every edit looked like it worked and none of them did.
|
|
1882
|
+
|
|
1883
|
+
The switch's remaining fields are sent back as they are, the way the
|
|
1884
|
+
page does (``_.pick(o, ['roleType', 'type', 'state', 'viewIds'])`` in
|
|
1885
|
+
``FormSet/containers/FunctionalSwitch/index.jsx``); dropping
|
|
1886
|
+
``viewIds`` would reset a view-scoped switch.
|
|
1887
|
+
|
|
1888
|
+
Because a stale payload fails silently rather than loudly, the new
|
|
1889
|
+
state is read back and a write that did not land raises instead of
|
|
1890
|
+
reporting success.
|
|
1891
|
+
"""
|
|
1892
|
+
current = {}
|
|
1893
|
+
for entry in get_switches(session, worksheet_id) or []:
|
|
1894
|
+
if isinstance(entry, dict) and entry.get("type") == switch_id:
|
|
1895
|
+
current = entry
|
|
1896
|
+
break
|
|
1897
|
+
payload: dict[str, Any] = {"worksheetId": worksheet_id,
|
|
1898
|
+
"type": switch_id, "state": value}
|
|
1899
|
+
for field in ("roleType", "viewIds", "displayFlowChart"):
|
|
1900
|
+
if field in current:
|
|
1901
|
+
payload[field] = current[field]
|
|
1902
|
+
payload.setdefault("roleType", 0)
|
|
1903
|
+
result = session.api_call("Worksheet", "EditSwitch", payload)
|
|
1904
|
+
applied = None
|
|
1905
|
+
for entry in get_switches(session, worksheet_id) or []:
|
|
1906
|
+
if isinstance(entry, dict) and entry.get("type") == switch_id:
|
|
1907
|
+
applied = entry.get("state")
|
|
1908
|
+
break
|
|
1909
|
+
if bool(applied) != bool(value):
|
|
1910
|
+
raise APIError(
|
|
1911
|
+
f"switch {switch_id} is still {applied!r} after asking for "
|
|
1912
|
+
f"{value!r} — the server reported success but did not apply it"
|
|
1913
|
+
)
|
|
1914
|
+
return result
|
|
1692
1915
|
|
|
1693
1916
|
|
|
1694
1917
|
# ── Logs ─────────────────────────────────────────────────────────────────
|
|
@@ -3969,6 +3969,18 @@
|
|
|
3969
3969
|
"en": "Raw JSON array of new controls to append. Each item matches the AddWorksheetControls `controls` schema (same shape as `worksheet fields --raw` output).",
|
|
3970
3970
|
"zh-Hans": "要追加的新字段原始 JSON 数组。每一项遵循 AddWorksheetControls 的 `controls` 结构(与 `worksheet fields --raw` 输出形态相同)"
|
|
3971
3971
|
},
|
|
3972
|
+
"worksheet batch-edit-switch": {
|
|
3973
|
+
"en": "Change several feature switches on one or more worksheets.\n\nThe switches you do not name keep their current values. Turning the\n批量操作 or 统计 parent on or off carries its children with it. Each\nworksheet is read back after saving, and any switch that did not take\nis reported rather than counted as success.",
|
|
3974
|
+
"zh-Hans": "批量修改一张或多张工作表的功能开关。\n\n未指定的开关保持原值。开启或关闭「批量操作」「统计」父开关时,其子项会随之联动。每张表保存后都会回读校验,未生效的开关会被报出而不是计为成功。"
|
|
3975
|
+
},
|
|
3976
|
+
"worksheet batch-edit-switch --dry-run": {
|
|
3977
|
+
"en": "Show what would change on each worksheet, without saving.",
|
|
3978
|
+
"zh-Hans": "仅显示每张工作表将要修改的内容,不实际保存。"
|
|
3979
|
+
},
|
|
3980
|
+
"worksheet batch-edit-switch --set": {
|
|
3981
|
+
"en": "Switch to change as TYPE=true|false (repeatable). Numbers come from `hap worksheet switch-types`.",
|
|
3982
|
+
"zh-Hans": "要修改的开关,格式为 TYPE=true|false(可重复指定)。开关号见 `hap worksheet switch-types`。"
|
|
3983
|
+
},
|
|
3972
3984
|
"worksheet chart": {
|
|
3973
3985
|
"en": "Worksheet chart (统计图) management.",
|
|
3974
3986
|
"zh-Hans": "工作表统计图管理"
|
|
@@ -4198,8 +4210,8 @@
|
|
|
4198
4210
|
"zh-Hans": "跳过确认"
|
|
4199
4211
|
},
|
|
4200
4212
|
"worksheet edit-switch": {
|
|
4201
|
-
"en": "
|
|
4202
|
-
"zh-Hans": "
|
|
4213
|
+
"en": "Turn one feature switch on or off.\n\nSWITCH_ID is a switch number from `hap worksheet switch-types`. The\nnew state is read back, so a change that did not take is reported as\nan error instead of success. To change several switches, or the same\nones across many worksheets, use `worksheet batch-edit-switch`.",
|
|
4214
|
+
"zh-Hans": "开启或关闭单个功能开关。\n\nSWITCH_ID 为开关号,可用 `hap worksheet switch-types` 查看。修改后会回读校验,未生效将报错而不是报成功。要一次改多个开关、或对多张工作表改同一批开关,请用 `worksheet batch-edit-switch`。"
|
|
4203
4215
|
},
|
|
4204
4216
|
"worksheet field-types": {
|
|
4205
4217
|
"en": "Show field type constants as a reference table.\n\nLists the builder-style CODE names (Text/Number/Relation/...) you\npass via 'update-fields --fields' alongside the wire ControlTypeId\nand the legacy SCREAMING_SNAKE alias still accepted for backward\ncompatibility. Region maps to three ControlTypeIds — pass\n'regionLevel' in the field spec's `extra` to pick one.",
|
|
@@ -4705,9 +4717,17 @@
|
|
|
4705
4717
|
"en": "Rule type: 0=interact 1=validate 2=lock",
|
|
4706
4718
|
"zh-Hans": "规则类型:0=交互 1=校验 2=锁定"
|
|
4707
4719
|
},
|
|
4720
|
+
"worksheet switch-types": {
|
|
4721
|
+
"en": "Show the feature switch numbers with their names and groups.",
|
|
4722
|
+
"zh-Hans": "显示功能开关号及其对应的名称和分组。"
|
|
4723
|
+
},
|
|
4708
4724
|
"worksheet switches": {
|
|
4709
|
-
"en": "Get feature switches for a worksheet.",
|
|
4710
|
-
"zh-Hans": "
|
|
4725
|
+
"en": "Get feature switches for a worksheet.\n\nEach switch is listed with the name and group it has on the feature\nswitch page, because several numbers share a name and only the group\ntells them apart.",
|
|
4726
|
+
"zh-Hans": "获取工作表的功能开关。每个开关会附带它在功能开关页面上的名称和分组——因为多个开关号共用同一名称,只有分组能区分它们。"
|
|
4727
|
+
},
|
|
4728
|
+
"worksheet switches --raw": {
|
|
4729
|
+
"en": "Return the server's switch list unannotated.",
|
|
4730
|
+
"zh-Hans": "返回服务端原始开关列表,不附加名称和分组。"
|
|
4711
4731
|
},
|
|
4712
4732
|
"worksheet update": {
|
|
4713
4733
|
"en": "Update worksheet alias, description, sidebar name, or sidebar icon.\n\n`--name` and `--icon` change how the worksheet appears in the app\nsidebar; both require --app-id so the section can be resolved.",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Uploaded files must carry their download permission.
|
|
2
|
+
|
|
3
|
+
Reported as "日程/讨论里别人上传的文件下载不了": a colleague opening a file
|
|
4
|
+
attached through the CLI got *您权限不足,无法下载,请联系管理员或文件上传者*,
|
|
5
|
+
while the same file attached from the web UI downloaded fine.
|
|
6
|
+
|
|
7
|
+
The file store does not decide this — the *client* does, per file, when
|
|
8
|
+
it hands the upload result to whatever will hold it. pd-openweb does it
|
|
9
|
+
in ``formatResponseData`` (src/components/UploadFiles/utils.js:202-206):
|
|
10
|
+
every non-picture upload gets ``allowDown: true`` and an empty
|
|
11
|
+
``docVersionID``; pictures are left alone because the viewer treats them
|
|
12
|
+
as downloadable anyway (UploadFiles/File.js:724). Every attachment
|
|
13
|
+
payload the frontend sends — record comments, record attachment cells —
|
|
14
|
+
is that same object, stringified.
|
|
15
|
+
|
|
16
|
+
The CLI's uploaded-file descriptor never had the field, so anything it
|
|
17
|
+
attached was stored with the permission unset and only the uploader
|
|
18
|
+
could get the bytes back.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from unittest.mock import MagicMock, patch
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
from hap_cli.core import record as rec_mod
|
|
27
|
+
from hap_cli.core import upload as up_mod
|
|
28
|
+
from hap_cli.core.session import Session
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def session():
|
|
33
|
+
return Session(server_url="https://test.mingdao.com", auth_token="tok123")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _store:
|
|
37
|
+
"""Stand in for the credential + store round trip of an upload."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, ext=".pdf"):
|
|
40
|
+
self.ext = ext
|
|
41
|
+
|
|
42
|
+
def __enter__(self):
|
|
43
|
+
token = {"uptoken": "t", "key": f"doc/20260909/k{self.ext}",
|
|
44
|
+
"serverName": "https://doc.mingdao.com/",
|
|
45
|
+
"fileName": f"k{self.ext}",
|
|
46
|
+
"url": f"https://doc.mingdao.com/doc/k{self.ext}"}
|
|
47
|
+
self._patches = [
|
|
48
|
+
patch.object(up_mod, "get_upload_token", return_value=token),
|
|
49
|
+
patch.object(up_mod, "upload_host",
|
|
50
|
+
return_value="https://upload.qiniup.com/"),
|
|
51
|
+
patch.object(up_mod, "_post_to_store", return_value={
|
|
52
|
+
"key": f"doc/20260909/k{self.ext}", "etag": "e",
|
|
53
|
+
"bucket": "mdoc", "filePath": "doc/20260909/", "fileName": "k",
|
|
54
|
+
"fileExt": self.ext, "originalFileName": "spec",
|
|
55
|
+
}),
|
|
56
|
+
]
|
|
57
|
+
for p in self._patches:
|
|
58
|
+
p.start()
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __exit__(self, *exc):
|
|
62
|
+
for p in self._patches:
|
|
63
|
+
p.stop()
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestDescriptorCarriesDownloadPermission:
|
|
68
|
+
|
|
69
|
+
def test_document_upload_is_downloadable(self, session):
|
|
70
|
+
with _store(".pdf"):
|
|
71
|
+
d = up_mod.upload_bytes(session, content=b"x", name="spec.pdf")
|
|
72
|
+
assert d["allowDown"] is True
|
|
73
|
+
assert d["docVersionID"] == ""
|
|
74
|
+
|
|
75
|
+
def test_archive_upload_is_downloadable(self, session):
|
|
76
|
+
# The reported file was a 13 MB .zip attached to a calendar comment.
|
|
77
|
+
with _store(".zip"):
|
|
78
|
+
d = up_mod.upload_bytes(session, content=b"x", name="proto.zip")
|
|
79
|
+
assert d["allowDown"] is True
|
|
80
|
+
|
|
81
|
+
def test_picture_upload_leaves_the_field_alone(self, session):
|
|
82
|
+
# Mirrors formatResponseData: pictures get neither field.
|
|
83
|
+
with _store(".png"):
|
|
84
|
+
d = up_mod.upload_bytes(session, content=b"x", name="cover.png")
|
|
85
|
+
assert "allowDown" not in d
|
|
86
|
+
assert "docVersionID" not in d
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestConsumersKeepThePermission:
|
|
90
|
+
|
|
91
|
+
_META = {"md.global": {"APPInfo": {"worksheetRowAppID": "row-app-id"}}}
|
|
92
|
+
|
|
93
|
+
def test_record_comment_attachment_keeps_it(self):
|
|
94
|
+
session = MagicMock()
|
|
95
|
+
session.get_global_meta.return_value = self._META
|
|
96
|
+
session.api_call.return_value = {"code": 1, "data": {"discussionId": "d1"}}
|
|
97
|
+
rec_mod.add_discussion(
|
|
98
|
+
session, "ws1", "row1", "here you go", app_id="app1",
|
|
99
|
+
attachments=[{"fileID": "o_1", "fileExt": ".zip", "allowDown": True,
|
|
100
|
+
"docVersionID": ""}],
|
|
101
|
+
)
|
|
102
|
+
_, _, payload = session.api_call.call_args[0][:3]
|
|
103
|
+
assert json.loads(payload["attachments"])[0]["allowDown"] is True
|
|
104
|
+
|
|
105
|
+
def test_attachment_cell_keeps_it(self):
|
|
106
|
+
cell = up_mod.record_attachment_cell([
|
|
107
|
+
{"fileID": "o_1", "fileExt": ".zip", "originalFileName": "proto",
|
|
108
|
+
"allowDown": True, "docVersionID": ""},
|
|
109
|
+
])
|
|
110
|
+
assert cell["attachments"][0]["allowDown"] is True
|
|
111
|
+
assert cell["attachments"][0]["docVersionID"] == ""
|
|
112
|
+
|
|
113
|
+
def test_attachment_cell_leaves_a_picture_alone(self):
|
|
114
|
+
cell = up_mod.record_attachment_cell([
|
|
115
|
+
{"fileID": "o_2", "fileExt": ".png", "originalFileName": "cover"},
|
|
116
|
+
])
|
|
117
|
+
assert "allowDown" not in cell["attachments"][0]
|
|
118
|
+
assert "docVersionID" not in cell["attachments"][0]
|