@qingflow-tech/qingflow-app-builder-mcp 1.0.1 → 1.0.3
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.
- package/README.md +2 -2
- package/docs/local-agent-install.md +9 -3
- package/npm/lib/runtime.mjs +10 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +88 -184
- package/skills/qingflow-app-builder/references/create-app.md +15 -34
- package/skills/qingflow-app-builder/references/gotchas.md +3 -3
- package/skills/qingflow-app-builder/references/solution-playbooks.md +1 -2
- package/skills/qingflow-app-builder/references/tool-selection.md +9 -10
- package/src/qingflow_mcp/__init__.py +33 -1
- package/src/qingflow_mcp/builder_facade/models.py +14 -4
- package/src/qingflow_mcp/builder_facade/service.py +1582 -124
- package/src/qingflow_mcp/cli/commands/auth.py +69 -1
- package/src/qingflow_mcp/cli/commands/builder.py +4 -3
- package/src/qingflow_mcp/cli/commands/record.py +5 -5
- package/src/qingflow_mcp/cli/commands/task.py +74 -22
- package/src/qingflow_mcp/cli/commands/workspace.py +22 -0
- package/src/qingflow_mcp/cli/formatters.py +287 -48
- package/src/qingflow_mcp/cli/main.py +6 -1
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/config.py +8 -0
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/public_surface.py +11 -1
- package/src/qingflow_mcp/response_trim.py +380 -9
- package/src/qingflow_mcp/server.py +4 -0
- package/src/qingflow_mcp/server_app_builder.py +11 -1
- package/src/qingflow_mcp/server_app_user.py +24 -0
- package/src/qingflow_mcp/session_store.py +69 -15
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +48 -18
- package/src/qingflow_mcp/tools/app_tools.py +1 -0
- package/src/qingflow_mcp/tools/auth_tools.py +271 -12
- package/src/qingflow_mcp/tools/base.py +6 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
- package/src/qingflow_mcp/tools/import_tools.py +36 -2
- package/src/qingflow_mcp/tools/record_tools.py +410 -156
- package/src/qingflow_mcp/tools/resource_read_tools.py +114 -32
- package/src/qingflow_mcp/tools/task_context_tools.py +899 -141
- package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
from copy import deepcopy
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
8
|
+
import random
|
|
7
9
|
import re
|
|
10
|
+
import string
|
|
8
11
|
import tempfile
|
|
9
12
|
from typing import Any, cast
|
|
13
|
+
from urllib.parse import quote_plus, unquote_plus
|
|
10
14
|
from uuid import uuid4
|
|
11
15
|
|
|
12
16
|
from ..backend_client import BackendRequestContext
|
|
@@ -143,6 +147,7 @@ JUDGE_EQUAL_ANY = 9
|
|
|
143
147
|
JUDGE_FUZZY_MATCH = 19
|
|
144
148
|
JUDGE_INCLUDE_ANY = 20
|
|
145
149
|
DEFAULT_TYPE_RELATION = 2
|
|
150
|
+
DEFAULT_TYPE_FORMULA = 3
|
|
146
151
|
RELATION_TYPE_Q_LINKER = 2
|
|
147
152
|
RELATION_TYPE_CODE_BLOCK = 3
|
|
148
153
|
|
|
@@ -440,6 +445,7 @@ class AiBuilderFacade:
|
|
|
440
445
|
base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
|
|
441
446
|
summary = detail_result.get("summary") if isinstance(detail_result, dict) and isinstance(detail_result.get("summary"), dict) else {}
|
|
442
447
|
source = detail if detail else base
|
|
448
|
+
layout_tag_items = _select_package_layout_tag_items(detail=detail, base=base)
|
|
443
449
|
warnings: list[JSONObject] = []
|
|
444
450
|
if detail_read_error is not None:
|
|
445
451
|
warnings.append(
|
|
@@ -450,7 +456,7 @@ class AiBuilderFacade:
|
|
|
450
456
|
"http_status": detail_read_error.http_status,
|
|
451
457
|
}
|
|
452
458
|
)
|
|
453
|
-
public_items = _public_package_items_from_tag_items(
|
|
459
|
+
public_items = _public_package_items_from_tag_items(layout_tag_items)
|
|
454
460
|
item_count = summary.get("itemCount")
|
|
455
461
|
if not isinstance(item_count, int) or item_count < 0 or (item_count == 0 and public_items):
|
|
456
462
|
item_count = len(public_items)
|
|
@@ -508,6 +514,8 @@ class AiBuilderFacade:
|
|
|
508
514
|
}
|
|
509
515
|
effective_package_id = _coerce_positive_int(package_id)
|
|
510
516
|
created = False
|
|
517
|
+
create_result: JSONObject | None = None
|
|
518
|
+
update_result: JSONObject | None = None
|
|
511
519
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
512
520
|
|
|
513
521
|
if effective_package_id is None:
|
|
@@ -598,11 +606,36 @@ class AiBuilderFacade:
|
|
|
598
606
|
)
|
|
599
607
|
except VisibilityResolutionError:
|
|
600
608
|
expected_visibility = None
|
|
609
|
+
metadata_verified = True
|
|
610
|
+
if metadata_requested and update_result is not None:
|
|
611
|
+
metadata_verified = bool(update_result.get("verified"))
|
|
612
|
+
elif created and create_result is not None:
|
|
613
|
+
metadata_verified = bool(create_result.get("verified"))
|
|
614
|
+
layout_verified = True
|
|
615
|
+
if items is not None and layout_result is not None:
|
|
616
|
+
layout_verified = bool(layout_result.get("verified"))
|
|
617
|
+
response_verification: JSONObject = {
|
|
618
|
+
"package_exists": True,
|
|
619
|
+
"package_created": created,
|
|
620
|
+
"layout_applied": items is not None,
|
|
621
|
+
"metadata_verified": metadata_verified,
|
|
622
|
+
"layout_verified": layout_verified,
|
|
623
|
+
"visibility_verified": None
|
|
624
|
+
if expected_visibility is None
|
|
625
|
+
else _visibility_matches_expected(verification.get("visibility"), expected_visibility),
|
|
626
|
+
}
|
|
627
|
+
if isinstance(update_result, dict):
|
|
628
|
+
update_verification = update_result.get("verification")
|
|
629
|
+
if isinstance(update_verification, dict):
|
|
630
|
+
for key in ("package_name_verified", "package_icon_verified", "visibility_verified"):
|
|
631
|
+
if key in update_verification:
|
|
632
|
+
response_verification[key] = deepcopy(update_verification.get(key))
|
|
633
|
+
response_verified = metadata_verified and layout_verified and response_verification.get("visibility_verified") is not False
|
|
601
634
|
response: JSONObject = {
|
|
602
|
-
"status": "success",
|
|
635
|
+
"status": "success" if response_verified else "partial_success",
|
|
603
636
|
"error_code": None,
|
|
604
637
|
"recoverable": False,
|
|
605
|
-
"message": "applied package",
|
|
638
|
+
"message": "applied package" if response_verified else "applied package with unverified readback",
|
|
606
639
|
"normalized_args": normalized_args,
|
|
607
640
|
"missing_fields": [],
|
|
608
641
|
"allowed_values": {},
|
|
@@ -611,15 +644,8 @@ class AiBuilderFacade:
|
|
|
611
644
|
"suggested_next_call": None,
|
|
612
645
|
"noop": not (created or metadata_requested or items is not None),
|
|
613
646
|
"warnings": [],
|
|
614
|
-
"verification":
|
|
615
|
-
|
|
616
|
-
"package_created": created,
|
|
617
|
-
"layout_applied": items is not None,
|
|
618
|
-
"visibility_verified": None
|
|
619
|
-
if expected_visibility is None
|
|
620
|
-
else _visibility_matches_expected(verification.get("visibility"), expected_visibility),
|
|
621
|
-
},
|
|
622
|
-
"verified": True,
|
|
647
|
+
"verification": response_verification,
|
|
648
|
+
"verified": response_verified,
|
|
623
649
|
**{
|
|
624
650
|
key: deepcopy(value)
|
|
625
651
|
for key, value in verification.items()
|
|
@@ -677,7 +703,7 @@ class AiBuilderFacade:
|
|
|
677
703
|
)
|
|
678
704
|
raw_current = current.get("result") if isinstance(current.get("result"), dict) else {}
|
|
679
705
|
raw_current_base = current_base.get("result") if isinstance(current_base.get("result"), dict) else {}
|
|
680
|
-
current_name = str(raw_current.get("tagName") or "").strip() or None
|
|
706
|
+
current_name = str(raw_current.get("tagName") or raw_current_base.get("tagName") or "").strip() or None
|
|
681
707
|
desired_name = str(package_name or current_name or "").strip() or current_name or "未命名应用包"
|
|
682
708
|
desired_icon = encode_workspace_icon_with_defaults(
|
|
683
709
|
icon=icon,
|
|
@@ -718,27 +744,33 @@ class AiBuilderFacade:
|
|
|
718
744
|
verification = self.package_get(profile=profile, package_id=tag_id)
|
|
719
745
|
if verification.get("status") != "success":
|
|
720
746
|
return verification
|
|
747
|
+
package_name_verified = str(verification.get("package_name") or "").strip() == desired_name
|
|
748
|
+
package_icon_verified = str(verification.get("icon") or "").strip() == desired_icon
|
|
749
|
+
visibility_verified = _visibility_matches_expected(
|
|
750
|
+
verification.get("visibility"),
|
|
751
|
+
_public_visibility_from_member_auth(desired_auth),
|
|
752
|
+
)
|
|
753
|
+
verified = package_name_verified and package_icon_verified and visibility_verified
|
|
721
754
|
return {
|
|
722
|
-
"status": "success",
|
|
755
|
+
"status": "success" if verified else "partial_success",
|
|
723
756
|
"error_code": None,
|
|
724
757
|
"recoverable": False,
|
|
725
|
-
"message": "updated package",
|
|
758
|
+
"message": "updated package" if verified else "updated package with unverified readback",
|
|
726
759
|
"normalized_args": normalized_args,
|
|
727
760
|
"missing_fields": [],
|
|
728
761
|
"allowed_values": {},
|
|
729
762
|
"details": {},
|
|
730
763
|
"request_id": update_result.get("request_id") if isinstance(update_result, dict) else None,
|
|
731
|
-
"suggested_next_call": None,
|
|
764
|
+
"suggested_next_call": None if verified else {"tool_name": "package_get", "arguments": {"profile": profile, "package_id": tag_id}},
|
|
732
765
|
"noop": False,
|
|
733
766
|
"warnings": [],
|
|
734
767
|
"verification": {
|
|
735
768
|
"package_exists": True,
|
|
736
|
-
"
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
),
|
|
769
|
+
"package_name_verified": package_name_verified,
|
|
770
|
+
"package_icon_verified": package_icon_verified,
|
|
771
|
+
"visibility_verified": visibility_verified,
|
|
740
772
|
},
|
|
741
|
-
"verified":
|
|
773
|
+
"verified": verified,
|
|
742
774
|
**{
|
|
743
775
|
key: deepcopy(value)
|
|
744
776
|
for key, value in verification.items()
|
|
@@ -881,9 +913,7 @@ class AiBuilderFacade:
|
|
|
881
913
|
if isinstance(current_base_result, dict) and isinstance(current_base_result.get("result"), dict)
|
|
882
914
|
else {}
|
|
883
915
|
)
|
|
884
|
-
|
|
885
|
-
base_tag_items = base_raw.get("tagItems") if isinstance(base_raw.get("tagItems"), list) else None
|
|
886
|
-
raw_tag_items = detail_tag_items if detail_tag_items else base_tag_items
|
|
916
|
+
raw_tag_items = _select_package_layout_tag_items(detail=detail_raw, base=base_raw)
|
|
887
917
|
if not isinstance(raw_tag_items, list):
|
|
888
918
|
return _failed(
|
|
889
919
|
"PACKAGE_LAYOUT_UNREADABLE",
|
|
@@ -1409,6 +1439,8 @@ class AiBuilderFacade:
|
|
|
1409
1439
|
issues: list[dict[str, Any]] = []
|
|
1410
1440
|
resolved: list[dict[str, Any]] = []
|
|
1411
1441
|
seen_ids: set[int] = set()
|
|
1442
|
+
if not dept_ids and not dept_names:
|
|
1443
|
+
return {"department_entries": resolved, "issues": issues}
|
|
1412
1444
|
listed = self.directory.directory_list_all_departments(
|
|
1413
1445
|
profile=profile,
|
|
1414
1446
|
parent_dept_id=None,
|
|
@@ -2780,6 +2812,19 @@ class AiBuilderFacade:
|
|
|
2780
2812
|
"can_copy_app": _coerce_optional_bool(base.get("copyAppStatus")),
|
|
2781
2813
|
}
|
|
2782
2814
|
|
|
2815
|
+
def _derive_can_edit_app_base(self, *, profile: str, permission_summary: JSONObject) -> bool:
|
|
2816
|
+
if permission_summary.get("can_edit_app") is not True:
|
|
2817
|
+
return False
|
|
2818
|
+
tag_ids = _coerce_int_list(permission_summary.get("tag_ids"))
|
|
2819
|
+
for tag_id in tag_ids:
|
|
2820
|
+
try:
|
|
2821
|
+
package_permission = self._read_package_permission_summary(profile=profile, tag_id=tag_id)
|
|
2822
|
+
except (QingflowApiError, RuntimeError):
|
|
2823
|
+
return False
|
|
2824
|
+
if package_permission.get("can_edit_tag") is not True:
|
|
2825
|
+
return False
|
|
2826
|
+
return True
|
|
2827
|
+
|
|
2783
2828
|
def _read_portal_permission_summary(self, *, dash_key: str, portal_result: dict[str, Any]) -> JSONObject:
|
|
2784
2829
|
tag_ids = _coerce_int_list(portal_result.get("tagIds"))
|
|
2785
2830
|
if not tag_ids:
|
|
@@ -2993,7 +3038,7 @@ class AiBuilderFacade:
|
|
|
2993
3038
|
|
|
2994
3039
|
def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
2995
3040
|
try:
|
|
2996
|
-
|
|
3041
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
2997
3042
|
except (QingflowApiError, RuntimeError) as error:
|
|
2998
3043
|
api_error = _coerce_api_error(error)
|
|
2999
3044
|
return _failed_from_api_error(
|
|
@@ -3003,26 +3048,55 @@ class AiBuilderFacade:
|
|
|
3003
3048
|
details={"app_key": app_key},
|
|
3004
3049
|
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3005
3050
|
)
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3051
|
+
base_result = base.get("result") if isinstance(base.get("result"), dict) else {}
|
|
3052
|
+
schema_unavailable = False
|
|
3053
|
+
try:
|
|
3054
|
+
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
3055
|
+
parsed = _parse_schema(schema_result)
|
|
3056
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3057
|
+
api_error = _coerce_api_error(error)
|
|
3058
|
+
if api_error.http_status == 404 or _is_permission_restricted_api_error(api_error):
|
|
3059
|
+
schema_unavailable = True
|
|
3060
|
+
parsed = {"fields": [], "layout": {"sections": []}}
|
|
3061
|
+
else:
|
|
3062
|
+
return _failed_from_api_error(
|
|
3063
|
+
"APP_READ_FAILED",
|
|
3064
|
+
api_error,
|
|
3065
|
+
normalized_args={"app_key": app_key},
|
|
3066
|
+
details={"app_key": app_key},
|
|
3067
|
+
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3068
|
+
)
|
|
3069
|
+
views, views_unavailable = self._load_views_result(
|
|
3070
|
+
profile=profile,
|
|
3071
|
+
app_key=app_key,
|
|
3072
|
+
tolerate_404=True,
|
|
3073
|
+
tolerate_permission_restricted=True,
|
|
3074
|
+
)
|
|
3075
|
+
workflow, workflow_unavailable = self._load_workflow_result(
|
|
3076
|
+
profile=profile,
|
|
3077
|
+
app_key=app_key,
|
|
3078
|
+
tolerate_404=True,
|
|
3079
|
+
tolerate_permission_restricted=True,
|
|
3080
|
+
)
|
|
3009
3081
|
verification_hints = _build_verification_hints(
|
|
3010
|
-
tag_ids=_coerce_int_list(
|
|
3082
|
+
tag_ids=_coerce_int_list(base_result.get("tagIds")),
|
|
3011
3083
|
fields=parsed["fields"],
|
|
3012
3084
|
layout=parsed["layout"],
|
|
3013
3085
|
views=_summarize_views(views),
|
|
3014
3086
|
)
|
|
3087
|
+
if schema_unavailable:
|
|
3088
|
+
verification_hints.append("schema_read_unavailable")
|
|
3015
3089
|
if views_unavailable:
|
|
3016
3090
|
verification_hints.append("views_read_unavailable")
|
|
3017
3091
|
if workflow_unavailable:
|
|
3018
3092
|
verification_hints.append("workflow_read_unavailable")
|
|
3019
3093
|
response = AppReadSummaryResponse(
|
|
3020
3094
|
app_key=app_key,
|
|
3021
|
-
title=
|
|
3022
|
-
app_icon=str(
|
|
3023
|
-
visibility=_public_visibility_from_member_auth(
|
|
3024
|
-
tag_ids=_coerce_int_list(
|
|
3025
|
-
publish_status=
|
|
3095
|
+
title=base_result.get("formTitle"),
|
|
3096
|
+
app_icon=str(base_result.get("appIcon") or "").strip() or None,
|
|
3097
|
+
visibility=_public_visibility_from_member_auth(base_result.get("auth")),
|
|
3098
|
+
tag_ids=_coerce_int_list(base_result.get("tagIds")),
|
|
3099
|
+
publish_status=base_result.get("appPublishStatus"),
|
|
3026
3100
|
field_count=len(parsed["fields"]),
|
|
3027
3101
|
layout_section_count=len(parsed["layout"].get("sections", [])),
|
|
3028
3102
|
view_count=len(_summarize_views(views)),
|
|
@@ -3044,10 +3118,11 @@ class AiBuilderFacade:
|
|
|
3044
3118
|
"warnings": _warnings_from_verification_hints(verification_hints),
|
|
3045
3119
|
"verification": {
|
|
3046
3120
|
"app_exists": True,
|
|
3121
|
+
"schema_read_unavailable": schema_unavailable,
|
|
3047
3122
|
"views_read_unavailable": views_unavailable,
|
|
3048
3123
|
"workflow_read_unavailable": workflow_unavailable,
|
|
3049
3124
|
},
|
|
3050
|
-
"verified": not views_unavailable and not workflow_unavailable,
|
|
3125
|
+
"verified": not schema_unavailable and not views_unavailable and not workflow_unavailable,
|
|
3051
3126
|
**response.model_dump(mode="json"),
|
|
3052
3127
|
}
|
|
3053
3128
|
|
|
@@ -3060,8 +3135,9 @@ class AiBuilderFacade:
|
|
|
3060
3135
|
permission_summary = self._read_app_permission_summary(profile=profile, app_key=app_key)
|
|
3061
3136
|
result["message"] = "read app config summary"
|
|
3062
3137
|
result["editability"] = {
|
|
3138
|
+
"can_edit_app_base": self._derive_can_edit_app_base(profile=profile, permission_summary=permission_summary),
|
|
3063
3139
|
"can_edit_form": permission_summary.get("can_edit_app"),
|
|
3064
|
-
"can_edit_flow": permission_summary.get("
|
|
3140
|
+
"can_edit_flow": permission_summary.get("can_manage_data"),
|
|
3065
3141
|
"can_edit_views": permission_summary.get("can_manage_data"),
|
|
3066
3142
|
"can_edit_charts": permission_summary.get("can_manage_data"),
|
|
3067
3143
|
}
|
|
@@ -4630,7 +4706,19 @@ class AiBuilderFacade:
|
|
|
4630
4706
|
)
|
|
4631
4707
|
field = current_fields[matched]
|
|
4632
4708
|
previous_name = field["name"]
|
|
4633
|
-
|
|
4709
|
+
try:
|
|
4710
|
+
_apply_field_mutation(field, patch.set)
|
|
4711
|
+
except ValueError as error:
|
|
4712
|
+
return _failed(
|
|
4713
|
+
"VALIDATION_ERROR",
|
|
4714
|
+
str(error),
|
|
4715
|
+
normalized_args=normalized_args,
|
|
4716
|
+
details={
|
|
4717
|
+
"selector": patch.selector.model_dump(mode="json"),
|
|
4718
|
+
"app_key": target.app_key,
|
|
4719
|
+
},
|
|
4720
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
4721
|
+
)
|
|
4634
4722
|
current_fields[matched] = field
|
|
4635
4723
|
layout = _rename_field_in_layout(layout, previous_name, field["name"])
|
|
4636
4724
|
updated.append(field["name"])
|
|
@@ -4789,12 +4877,22 @@ class AiBuilderFacade:
|
|
|
4789
4877
|
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
4790
4878
|
return finalize(self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response))
|
|
4791
4879
|
|
|
4792
|
-
payload =
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4880
|
+
payload = (
|
|
4881
|
+
_build_form_payload_from_fields(
|
|
4882
|
+
title=effective_app_name,
|
|
4883
|
+
current_schema=schema_result,
|
|
4884
|
+
fields=current_fields,
|
|
4885
|
+
layout=layout,
|
|
4886
|
+
question_relations=compiled_question_relations,
|
|
4887
|
+
)
|
|
4888
|
+
if bool(resolved.get("created"))
|
|
4889
|
+
else _build_form_payload_for_edit_fields(
|
|
4890
|
+
title=effective_app_name,
|
|
4891
|
+
current_schema=schema_result,
|
|
4892
|
+
fields=current_fields,
|
|
4893
|
+
layout=layout,
|
|
4894
|
+
question_relations=compiled_question_relations,
|
|
4895
|
+
)
|
|
4798
4896
|
)
|
|
4799
4897
|
payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
4800
4898
|
profile=profile,
|
|
@@ -4897,12 +4995,22 @@ class AiBuilderFacade:
|
|
|
4897
4995
|
},
|
|
4898
4996
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
4899
4997
|
)
|
|
4900
|
-
rebound_payload =
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4998
|
+
rebound_payload = (
|
|
4999
|
+
_build_form_payload_from_fields(
|
|
5000
|
+
title=effective_app_name,
|
|
5001
|
+
current_schema=rebound_schema,
|
|
5002
|
+
fields=rebound_fields,
|
|
5003
|
+
layout=rebound_layout,
|
|
5004
|
+
question_relations=compiled_question_relations,
|
|
5005
|
+
)
|
|
5006
|
+
if bool(resolved.get("created"))
|
|
5007
|
+
else _build_form_payload_for_edit_fields(
|
|
5008
|
+
title=effective_app_name,
|
|
5009
|
+
current_schema=rebound_schema,
|
|
5010
|
+
fields=rebound_fields,
|
|
5011
|
+
layout=rebound_layout,
|
|
5012
|
+
question_relations=compiled_question_relations,
|
|
5013
|
+
)
|
|
4906
5014
|
)
|
|
4907
5015
|
rebound_payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
4908
5016
|
profile=profile,
|
|
@@ -6670,6 +6778,7 @@ class AiBuilderFacade:
|
|
|
6670
6778
|
|
|
6671
6779
|
for patch in request.upsert_charts:
|
|
6672
6780
|
try:
|
|
6781
|
+
config_update_requested = _chart_patch_updates_chart_config(patch)
|
|
6673
6782
|
chart_visible_auth = (
|
|
6674
6783
|
self._compile_visibility_to_chart_visible_auth(profile=profile, visibility=patch.visibility)
|
|
6675
6784
|
if patch.visibility is not None
|
|
@@ -6773,18 +6882,17 @@ class AiBuilderFacade:
|
|
|
6773
6882
|
existing_by_name.pop(old_name, None)
|
|
6774
6883
|
existing_by_name.setdefault(patch.name, []).append(deepcopy(updated_chart))
|
|
6775
6884
|
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
if existing is not None and chart_id not in updated_ids:
|
|
6885
|
+
config_updated = False
|
|
6886
|
+
if existing is None or config_update_requested:
|
|
6887
|
+
config_payload = _build_public_chart_config_payload(
|
|
6888
|
+
patch=patch,
|
|
6889
|
+
app_key=app_key,
|
|
6890
|
+
field_lookup=field_lookup,
|
|
6891
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
6892
|
+
)
|
|
6893
|
+
self.charts.qingbi_report_update_config(profile=profile, chart_id=chart_id, payload=config_payload)
|
|
6894
|
+
config_updated = True
|
|
6895
|
+
if existing is not None and chart_id not in updated_ids and config_updated:
|
|
6788
6896
|
updated_ids.append(chart_id)
|
|
6789
6897
|
if patch.question_config:
|
|
6790
6898
|
self._request_backend(
|
|
@@ -6800,6 +6908,8 @@ class AiBuilderFacade:
|
|
|
6800
6908
|
path=f"/chart/{chart_id}/user/config",
|
|
6801
6909
|
json_body=patch.user_config,
|
|
6802
6910
|
)
|
|
6911
|
+
if existing is not None and chart_id not in updated_ids and (patch.question_config or patch.user_config):
|
|
6912
|
+
updated_ids.append(chart_id)
|
|
6803
6913
|
chart_results.append(
|
|
6804
6914
|
{
|
|
6805
6915
|
"chart_id": chart_id,
|
|
@@ -6956,11 +7066,12 @@ class AiBuilderFacade:
|
|
|
6956
7066
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
6957
7067
|
dash_key = str(request.dash_key or "").strip()
|
|
6958
7068
|
creating = not dash_key
|
|
7069
|
+
sections_requested = creating or bool(request.sections)
|
|
6959
7070
|
verify_dash_name = creating or request.dash_name is not None
|
|
6960
7071
|
verify_dash_icon = bool(request.icon or request.color)
|
|
6961
7072
|
verify_auth = request.visibility is not None or request.auth is not None
|
|
6962
|
-
verify_hide_copyright = request.hide_copyright is not None
|
|
6963
|
-
verify_dash_global_config = request.dash_global_config is not None
|
|
7073
|
+
verify_hide_copyright = request.hide_copyright is not None and sections_requested
|
|
7074
|
+
verify_dash_global_config = request.dash_global_config is not None and sections_requested
|
|
6964
7075
|
verify_tags = creating or request.package_tag_id is not None
|
|
6965
7076
|
requested_visibility = request.visibility
|
|
6966
7077
|
if requested_visibility is None and isinstance(request.auth, dict) and request.auth:
|
|
@@ -7037,6 +7148,25 @@ class AiBuilderFacade:
|
|
|
7037
7148
|
if package_edit_outcome.block is not None:
|
|
7038
7149
|
return package_edit_outcome.block
|
|
7039
7150
|
permission_outcomes.append(package_edit_outcome)
|
|
7151
|
+
if not sections_requested:
|
|
7152
|
+
unsupported_base_only_keys: list[str] = []
|
|
7153
|
+
if request.hide_copyright is not None:
|
|
7154
|
+
unsupported_base_only_keys.append("hide_copyright")
|
|
7155
|
+
if request.dash_global_config is not None:
|
|
7156
|
+
unsupported_base_only_keys.append("dash_global_config")
|
|
7157
|
+
if request.config:
|
|
7158
|
+
unsupported_base_only_keys.append("config")
|
|
7159
|
+
if unsupported_base_only_keys:
|
|
7160
|
+
return _failed(
|
|
7161
|
+
"PORTAL_SECTIONS_REQUIRED",
|
|
7162
|
+
"editing a portal without sections only supports base-info updates",
|
|
7163
|
+
normalized_args=normalized_args,
|
|
7164
|
+
details={
|
|
7165
|
+
"unsupported_without_sections": unsupported_base_only_keys,
|
|
7166
|
+
"fix_hint": "Pass sections when changing layout or global portal config, or omit those keys for visibility/icon/package updates.",
|
|
7167
|
+
},
|
|
7168
|
+
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7169
|
+
)
|
|
7040
7170
|
try:
|
|
7041
7171
|
if creating:
|
|
7042
7172
|
create_payload = _build_public_portal_base_payload(
|
|
@@ -7062,7 +7192,6 @@ class AiBuilderFacade:
|
|
|
7062
7192
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7063
7193
|
)
|
|
7064
7194
|
base_payload = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
|
|
7065
|
-
component_payload = self._build_portal_components_from_sections(profile=profile, sections=request.sections)
|
|
7066
7195
|
update_payload = _build_public_portal_base_payload(
|
|
7067
7196
|
dash_name=request.dash_name or str(base_payload.get("dashName") or "").strip() or "未命名门户",
|
|
7068
7197
|
package_tag_id=target_package_tag_id,
|
|
@@ -7074,8 +7203,10 @@ class AiBuilderFacade:
|
|
|
7074
7203
|
config=request.config,
|
|
7075
7204
|
base_payload=base_payload,
|
|
7076
7205
|
)
|
|
7077
|
-
|
|
7078
|
-
|
|
7206
|
+
if sections_requested:
|
|
7207
|
+
component_payload = self._build_portal_components_from_sections(profile=profile, sections=request.sections)
|
|
7208
|
+
update_payload["components"] = component_payload
|
|
7209
|
+
self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
7079
7210
|
self.portals.portal_update_base_info(
|
|
7080
7211
|
profile=profile,
|
|
7081
7212
|
dash_key=dash_key,
|
|
@@ -7112,11 +7243,14 @@ class AiBuilderFacade:
|
|
|
7112
7243
|
publish_failed = True
|
|
7113
7244
|
|
|
7114
7245
|
draft_components = draft_result.get("components") if isinstance(draft_result, dict) else None
|
|
7115
|
-
expected_count = len(request.sections)
|
|
7116
|
-
draft_verified = isinstance(
|
|
7246
|
+
expected_count = len(request.sections) if sections_requested else None
|
|
7247
|
+
draft_verified = isinstance(draft_result, dict) and (
|
|
7248
|
+
expected_count is None or (isinstance(draft_components, list) and len(draft_components) == expected_count)
|
|
7249
|
+
)
|
|
7117
7250
|
draft_meta_verified, draft_meta_mismatches = _verify_portal_readback(
|
|
7118
7251
|
actual=draft_result,
|
|
7119
7252
|
expected_payload=update_payload,
|
|
7253
|
+
expected_visibility=requested_visibility.model_dump(mode="json") if requested_visibility is not None else None,
|
|
7120
7254
|
expected_section_count=expected_count,
|
|
7121
7255
|
requested_config_keys=set((request.config or {}).keys()),
|
|
7122
7256
|
verify_dash_name=verify_dash_name,
|
|
@@ -7132,12 +7266,18 @@ class AiBuilderFacade:
|
|
|
7132
7266
|
if request.publish:
|
|
7133
7267
|
live_verified = (
|
|
7134
7268
|
isinstance(live_result, dict)
|
|
7135
|
-
and
|
|
7136
|
-
|
|
7269
|
+
and (
|
|
7270
|
+
expected_count is None
|
|
7271
|
+
or (
|
|
7272
|
+
isinstance(live_result.get("components"), list)
|
|
7273
|
+
and len(live_result.get("components")) == expected_count
|
|
7274
|
+
)
|
|
7275
|
+
)
|
|
7137
7276
|
)
|
|
7138
7277
|
live_meta_verified, live_meta_mismatches = _verify_portal_readback(
|
|
7139
7278
|
actual=live_result,
|
|
7140
7279
|
expected_payload=update_payload,
|
|
7280
|
+
expected_visibility=requested_visibility.model_dump(mode="json") if requested_visibility is not None else None,
|
|
7141
7281
|
expected_section_count=expected_count,
|
|
7142
7282
|
requested_config_keys=set((request.config or {}).keys()),
|
|
7143
7283
|
verify_dash_name=verify_dash_name,
|
|
@@ -7171,7 +7311,15 @@ class AiBuilderFacade:
|
|
|
7171
7311
|
"status": status,
|
|
7172
7312
|
"error_code": error_code,
|
|
7173
7313
|
"recoverable": not verified,
|
|
7174
|
-
"message":
|
|
7314
|
+
"message": (
|
|
7315
|
+
"updated portal base info"
|
|
7316
|
+
if verified and not sections_requested
|
|
7317
|
+
else "applied portal"
|
|
7318
|
+
if verified
|
|
7319
|
+
else "updated portal base info; draft/live verification pending"
|
|
7320
|
+
if not sections_requested
|
|
7321
|
+
else "applied portal; draft/live verification pending"
|
|
7322
|
+
),
|
|
7175
7323
|
"normalized_args": normalized_args,
|
|
7176
7324
|
"missing_fields": [],
|
|
7177
7325
|
"allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"]},
|
|
@@ -7366,17 +7514,35 @@ class AiBuilderFacade:
|
|
|
7366
7514
|
sync_result = {**sync_result, "button_config_restored": True}
|
|
7367
7515
|
return sync_result
|
|
7368
7516
|
|
|
7369
|
-
def _load_views_result(
|
|
7517
|
+
def _load_views_result(
|
|
7518
|
+
self,
|
|
7519
|
+
*,
|
|
7520
|
+
profile: str,
|
|
7521
|
+
app_key: str,
|
|
7522
|
+
tolerate_404: bool,
|
|
7523
|
+
tolerate_permission_restricted: bool = False,
|
|
7524
|
+
) -> tuple[Any, bool]:
|
|
7370
7525
|
try:
|
|
7371
7526
|
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
7372
7527
|
except (QingflowApiError, RuntimeError) as error:
|
|
7373
7528
|
api_error = _coerce_api_error(error)
|
|
7374
|
-
if api_error.http_status == 404
|
|
7529
|
+
if api_error.http_status == 404 or (
|
|
7530
|
+
tolerate_permission_restricted and _is_permission_restricted_api_error(api_error)
|
|
7531
|
+
):
|
|
7375
7532
|
try:
|
|
7376
7533
|
legacy_views = self.views.view_list(profile=profile, app_key=app_key)
|
|
7377
7534
|
except (QingflowApiError, RuntimeError) as legacy_error:
|
|
7378
7535
|
legacy_api_error = _coerce_api_error(legacy_error)
|
|
7379
|
-
if
|
|
7536
|
+
if (
|
|
7537
|
+
tolerate_404
|
|
7538
|
+
and (
|
|
7539
|
+
legacy_api_error.http_status == 404
|
|
7540
|
+
or (
|
|
7541
|
+
tolerate_permission_restricted
|
|
7542
|
+
and _is_permission_restricted_api_error(legacy_api_error)
|
|
7543
|
+
)
|
|
7544
|
+
)
|
|
7545
|
+
):
|
|
7380
7546
|
return [], True
|
|
7381
7547
|
raise
|
|
7382
7548
|
legacy_result = legacy_views.get("result")
|
|
@@ -7393,19 +7559,38 @@ class AiBuilderFacade:
|
|
|
7393
7559
|
legacy_views = self.views.view_list(profile=profile, app_key=app_key)
|
|
7394
7560
|
except (QingflowApiError, RuntimeError) as legacy_error:
|
|
7395
7561
|
legacy_api_error = _coerce_api_error(legacy_error)
|
|
7396
|
-
if
|
|
7562
|
+
if (
|
|
7563
|
+
tolerate_404
|
|
7564
|
+
and (
|
|
7565
|
+
legacy_api_error.http_status == 404
|
|
7566
|
+
or (
|
|
7567
|
+
tolerate_permission_restricted
|
|
7568
|
+
and _is_permission_restricted_api_error(legacy_api_error)
|
|
7569
|
+
)
|
|
7570
|
+
)
|
|
7571
|
+
):
|
|
7397
7572
|
return normalized_views, False
|
|
7398
7573
|
raise
|
|
7399
7574
|
legacy_result = legacy_views.get("result")
|
|
7400
7575
|
legacy_normalized = _normalize_view_collection(legacy_result)
|
|
7401
7576
|
return legacy_normalized or normalized_views, False
|
|
7402
7577
|
|
|
7403
|
-
def _load_workflow_result(
|
|
7578
|
+
def _load_workflow_result(
|
|
7579
|
+
self,
|
|
7580
|
+
*,
|
|
7581
|
+
profile: str,
|
|
7582
|
+
app_key: str,
|
|
7583
|
+
tolerate_404: bool,
|
|
7584
|
+
tolerate_permission_restricted: bool = False,
|
|
7585
|
+
) -> tuple[Any, bool]:
|
|
7404
7586
|
try:
|
|
7405
7587
|
workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
7406
7588
|
except (QingflowApiError, RuntimeError) as error:
|
|
7407
7589
|
api_error = _coerce_api_error(error)
|
|
7408
|
-
if tolerate_404 and
|
|
7590
|
+
if tolerate_404 and (
|
|
7591
|
+
api_error.http_status == 404
|
|
7592
|
+
or (tolerate_permission_restricted and _is_permission_restricted_api_error(api_error))
|
|
7593
|
+
):
|
|
7409
7594
|
return [], True
|
|
7410
7595
|
raise
|
|
7411
7596
|
return workflow.get("result"), False
|
|
@@ -8697,6 +8882,11 @@ def _build_public_chart_config_payload(
|
|
|
8697
8882
|
return payload
|
|
8698
8883
|
|
|
8699
8884
|
|
|
8885
|
+
def _chart_patch_updates_chart_config(patch: ChartUpsertPatch) -> bool:
|
|
8886
|
+
explicit_fields = set(getattr(patch, "model_fields_set", set()) or set())
|
|
8887
|
+
return bool({"dimension_field_ids", "indicator_field_ids", "filters", "config"} & explicit_fields)
|
|
8888
|
+
|
|
8889
|
+
|
|
8700
8890
|
def _build_public_portal_base_payload(
|
|
8701
8891
|
*,
|
|
8702
8892
|
dash_name: str,
|
|
@@ -8926,7 +9116,8 @@ def _verify_portal_readback(
|
|
|
8926
9116
|
*,
|
|
8927
9117
|
actual: Any,
|
|
8928
9118
|
expected_payload: dict[str, Any],
|
|
8929
|
-
|
|
9119
|
+
expected_visibility: dict[str, Any] | None,
|
|
9120
|
+
expected_section_count: int | None,
|
|
8930
9121
|
requested_config_keys: set[str],
|
|
8931
9122
|
verify_dash_name: bool,
|
|
8932
9123
|
verify_dash_icon: bool,
|
|
@@ -8939,14 +9130,19 @@ def _verify_portal_readback(
|
|
|
8939
9130
|
if not isinstance(actual, dict):
|
|
8940
9131
|
return False, ["portal readback payload is unavailable"]
|
|
8941
9132
|
components = actual.get("components")
|
|
8942
|
-
if not isinstance(components, list) or len(components) != expected_section_count:
|
|
9133
|
+
if expected_section_count is not None and (not isinstance(components, list) or len(components) != expected_section_count):
|
|
8943
9134
|
mismatches.append(f"components expected {expected_section_count}, got {len(components) if isinstance(components, list) else 'unavailable'}")
|
|
8944
9135
|
if verify_dash_name and str(actual.get("dashName") or "").strip() != str(expected_payload.get("dashName") or "").strip():
|
|
8945
9136
|
mismatches.append("dash_name")
|
|
8946
9137
|
if verify_dash_icon and str(actual.get("dashIcon") or "") != str(expected_payload.get("dashIcon") or ""):
|
|
8947
9138
|
mismatches.append("dash_icon")
|
|
8948
|
-
if verify_auth
|
|
8949
|
-
|
|
9139
|
+
if verify_auth:
|
|
9140
|
+
if expected_visibility is not None:
|
|
9141
|
+
actual_visibility = _public_visibility_from_member_auth(actual.get("auth"))
|
|
9142
|
+
if not _visibility_matches_expected(actual_visibility, expected_visibility):
|
|
9143
|
+
mismatches.append("auth")
|
|
9144
|
+
elif not _mapping_contains(actual.get("auth"), expected_payload.get("auth")):
|
|
9145
|
+
mismatches.append("auth")
|
|
8950
9146
|
if verify_hide_copyright and bool(actual.get("hideCopyright", False)) != bool(expected_payload.get("hideCopyright", False)):
|
|
8951
9147
|
mismatches.append("hide_copyright")
|
|
8952
9148
|
if verify_dash_global_config and not _mapping_contains(actual.get("dashGlobalConfig") or {}, expected_payload.get("dashGlobalConfig") or {}):
|
|
@@ -9154,7 +9350,11 @@ def _visibility_matches_expected(actual: Any, expected: Any) -> bool:
|
|
|
9154
9350
|
if expected_text and sorted_values(actual_group, text_key) != expected_text:
|
|
9155
9351
|
return False
|
|
9156
9352
|
|
|
9157
|
-
if
|
|
9353
|
+
if (
|
|
9354
|
+
"include_sub_departs" in expected_selectors
|
|
9355
|
+
and expected_selectors.get("include_sub_departs") is not None
|
|
9356
|
+
and actual_selectors.get("include_sub_departs") != expected_selectors.get("include_sub_departs")
|
|
9357
|
+
):
|
|
9158
9358
|
return False
|
|
9159
9359
|
return True
|
|
9160
9360
|
|
|
@@ -9611,6 +9811,15 @@ def _apply_relation_target_selection(
|
|
|
9611
9811
|
config["refer_field_types"] = [item.get("type") for item in normalized_visible]
|
|
9612
9812
|
config["auth_field_ids"] = [item.get("field_id") or item.get("name") for item in normalized_visible]
|
|
9613
9813
|
config["auth_field_que_ids"] = [_coerce_positive_int(item.get("que_id")) or 0 for item in normalized_visible]
|
|
9814
|
+
config["refer_auth_ques"] = [
|
|
9815
|
+
{
|
|
9816
|
+
"queId": _coerce_positive_int(item.get("que_id")) or 0,
|
|
9817
|
+
"queAuth": _REFERENCE_FIELD_VISIBLE_AUTH,
|
|
9818
|
+
"_field_id": item.get("field_id") or item.get("name"),
|
|
9819
|
+
}
|
|
9820
|
+
for item in normalized_visible
|
|
9821
|
+
if (_coerce_positive_int(item.get("que_id")) or 0) > 0
|
|
9822
|
+
]
|
|
9614
9823
|
config["field_name_show"] = bool(field.get("field_name_show", True))
|
|
9615
9824
|
field["target_field_id"] = display_field.get("field_id") or display_field.get("name")
|
|
9616
9825
|
field["target_field_que_id"] = _coerce_positive_int(display_field.get("que_id")) or 0
|
|
@@ -9844,9 +10053,10 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
9844
10053
|
"subfields": [],
|
|
9845
10054
|
"que_id": que_id,
|
|
9846
10055
|
"que_type": que_type,
|
|
9847
|
-
"default_type": _coerce_positive_int(question.get("queDefaultType"))
|
|
9848
|
-
"default_value": question.get("queDefaultValue"),
|
|
10056
|
+
"default_type": _coerce_positive_int(question.get("queDefaultType")) if "queDefaultType" in question else None,
|
|
9849
10057
|
}
|
|
10058
|
+
if "queDefaultValue" in question:
|
|
10059
|
+
field["default_value"] = question.get("queDefaultValue")
|
|
9850
10060
|
if field_type in {FieldType.single_select.value, FieldType.multi_select.value, FieldType.boolean.value}:
|
|
9851
10061
|
options = question.get("options")
|
|
9852
10062
|
if isinstance(options, list):
|
|
@@ -9870,17 +10080,32 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
9870
10080
|
field["target_app_key"] = reference.get("referAppKey")
|
|
9871
10081
|
field["relation_mode"] = _relation_mode_from_optional_data_num(reference.get("optionalDataNum"))
|
|
9872
10082
|
refer_questions = reference.get("referQuestions") if isinstance(reference.get("referQuestions"), list) else []
|
|
10083
|
+
refer_auth_questions = reference.get("referAuthQues") if isinstance(reference.get("referAuthQues"), list) else []
|
|
10084
|
+
refer_auth_by_que_id: dict[int, int] = {}
|
|
10085
|
+
for raw_item in refer_auth_questions:
|
|
10086
|
+
if not isinstance(raw_item, dict):
|
|
10087
|
+
continue
|
|
10088
|
+
que_id = _coerce_nonnegative_int(raw_item.get("queId"))
|
|
10089
|
+
que_auth = _coerce_nonnegative_int(raw_item.get("queAuth"))
|
|
10090
|
+
if que_id is None or que_auth is None or que_id in refer_auth_by_que_id:
|
|
10091
|
+
continue
|
|
10092
|
+
refer_auth_by_que_id[que_id] = que_auth
|
|
9873
10093
|
visible_fields: list[dict[str, Any]] = []
|
|
9874
10094
|
display_field_que_id = _coerce_nonnegative_int(reference.get("referQueId"))
|
|
9875
10095
|
display_field_name: str | None = None
|
|
9876
10096
|
for item in refer_questions:
|
|
9877
10097
|
if not isinstance(item, dict):
|
|
9878
10098
|
continue
|
|
10099
|
+
que_id = _coerce_nonnegative_int(item.get("queId"))
|
|
10100
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
10101
|
+
if que_auth is None and que_id is not None:
|
|
10102
|
+
que_auth = refer_auth_by_que_id.get(que_id)
|
|
9879
10103
|
selector = {
|
|
9880
|
-
"que_id":
|
|
10104
|
+
"que_id": que_id,
|
|
9881
10105
|
"name": str(item.get("queTitle") or "").strip() or None,
|
|
9882
10106
|
}
|
|
9883
|
-
|
|
10107
|
+
if que_auth != _REFERENCE_FIELD_HIDDEN_AUTH:
|
|
10108
|
+
visible_fields.append(selector)
|
|
9884
10109
|
if display_field_que_id is not None and selector["que_id"] == display_field_que_id:
|
|
9885
10110
|
display_field_name = selector["name"]
|
|
9886
10111
|
if display_field_name is None and visible_fields:
|
|
@@ -9960,6 +10185,7 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
9960
10185
|
continue
|
|
9961
10186
|
subfields.append(_parse_field(sub_question))
|
|
9962
10187
|
field["subfields"] = subfields
|
|
10188
|
+
field["_question_template"] = deepcopy(question)
|
|
9963
10189
|
return field
|
|
9964
10190
|
|
|
9965
10191
|
|
|
@@ -10505,14 +10731,14 @@ def _parse_code_block_inputs_and_body(code_content: str) -> tuple[list[dict[str,
|
|
|
10505
10731
|
return inputs, body
|
|
10506
10732
|
|
|
10507
10733
|
|
|
10508
|
-
def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any]) -> dict[str, Any]:
|
|
10734
|
+
def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any], nested: bool = False) -> dict[str, Any]:
|
|
10509
10735
|
payload = {
|
|
10510
10736
|
"field_id": field.get("field_id"),
|
|
10511
10737
|
"que_id": field.get("que_id"),
|
|
10512
10738
|
"name": field.get("name"),
|
|
10513
10739
|
"type": field.get("type"),
|
|
10514
10740
|
"required": bool(field.get("required")),
|
|
10515
|
-
"section_id": _find_field_section_id(layout, str(field.get("name") or "")),
|
|
10741
|
+
"section_id": None if nested else _find_field_section_id(layout, str(field.get("name") or "")),
|
|
10516
10742
|
}
|
|
10517
10743
|
if field.get("type") == FieldType.relation.value:
|
|
10518
10744
|
payload["target_app_key"] = field.get("target_app_key")
|
|
@@ -10545,6 +10771,12 @@ def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any])
|
|
|
10545
10771
|
payload["custom_button_text"] = field.get("custom_button_text")
|
|
10546
10772
|
if field.get("metadata_unverified") is not None:
|
|
10547
10773
|
payload["metadata_unverified"] = bool(field.get("metadata_unverified"))
|
|
10774
|
+
if field.get("type") == FieldType.subtable.value:
|
|
10775
|
+
payload["subfields"] = [
|
|
10776
|
+
_compact_public_field_read(field=subfield, layout=layout, nested=True)
|
|
10777
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or [])
|
|
10778
|
+
if isinstance(subfield, dict)
|
|
10779
|
+
]
|
|
10548
10780
|
return payload
|
|
10549
10781
|
|
|
10550
10782
|
|
|
@@ -10840,59 +11072,119 @@ def _code_block_binding_equal(left: Any, right: Any) -> bool:
|
|
|
10840
11072
|
return _normalize_code_block_binding(left) == _normalize_code_block_binding(right)
|
|
10841
11073
|
|
|
10842
11074
|
|
|
11075
|
+
_SAFE_SUBFIELD_MUTATION_KEYS = frozenset({"name", "required", "description", "subfield_updates"})
|
|
11076
|
+
|
|
11077
|
+
|
|
11078
|
+
def _validate_safe_subfield_mutation(*, payload: dict[str, Any], location: str) -> None:
|
|
11079
|
+
unsupported = sorted(key for key in payload if key not in _SAFE_SUBFIELD_MUTATION_KEYS)
|
|
11080
|
+
if unsupported:
|
|
11081
|
+
raise ValueError(
|
|
11082
|
+
f"{location} only supports safe overlay keys: name, required, description, subfield_updates; "
|
|
11083
|
+
f"unsupported keys: {', '.join(unsupported)}"
|
|
11084
|
+
)
|
|
11085
|
+
|
|
11086
|
+
|
|
11087
|
+
def _apply_subfield_updates(field: dict[str, Any], raw_updates: list[Any]) -> None:
|
|
11088
|
+
if str(field.get("type") or "") != FieldType.subtable.value:
|
|
11089
|
+
raise ValueError("subfield_updates can only target subtable fields")
|
|
11090
|
+
subfields = [subfield for subfield in cast(list[dict[str, Any]], field.get("subfields") or []) if isinstance(subfield, dict)]
|
|
11091
|
+
for index, raw_item in enumerate(raw_updates):
|
|
11092
|
+
patch = FieldUpdatePatch.model_validate(raw_item)
|
|
11093
|
+
payload = patch.set.model_dump(mode="json", exclude_none=True)
|
|
11094
|
+
_validate_safe_subfield_mutation(payload=payload, location=f"subfield_updates[{index}].set")
|
|
11095
|
+
target = _resolve_field_selector_with_uniqueness(
|
|
11096
|
+
fields=subfields,
|
|
11097
|
+
selector_payload=patch.selector.model_dump(mode="json", exclude_none=True),
|
|
11098
|
+
location=f"subfield_updates[{index}].selector",
|
|
11099
|
+
)
|
|
11100
|
+
_apply_field_mutation(target, patch.set)
|
|
11101
|
+
field["subfields"] = subfields
|
|
11102
|
+
|
|
11103
|
+
|
|
10843
11104
|
def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
10844
11105
|
payload = mutation.model_dump(mode="json", exclude_none=True)
|
|
10845
11106
|
relation_config_explicit = (
|
|
10846
11107
|
payload.get("type") == FieldType.relation.value
|
|
10847
11108
|
or any(key in payload for key in ("target_app_key", "display_field", "visible_fields", "relation_mode"))
|
|
10848
11109
|
)
|
|
11110
|
+
question_overlay_keys = set(cast(list[str], field.get("_question_overlay_keys") or []))
|
|
11111
|
+
question_rebuild_required = bool(field.get("_question_rebuild_required"))
|
|
10849
11112
|
if "name" in payload:
|
|
10850
11113
|
field["name"] = payload["name"]
|
|
11114
|
+
question_overlay_keys.add("name")
|
|
10851
11115
|
if "type" in payload:
|
|
10852
11116
|
field["type"] = payload["type"]
|
|
11117
|
+
question_rebuild_required = True
|
|
10853
11118
|
if "required" in payload:
|
|
10854
11119
|
field["required"] = payload["required"]
|
|
11120
|
+
question_overlay_keys.add("required")
|
|
10855
11121
|
if "description" in payload:
|
|
10856
11122
|
field["description"] = payload["description"]
|
|
11123
|
+
question_overlay_keys.add("description")
|
|
10857
11124
|
if "options" in payload:
|
|
10858
11125
|
field["options"] = list(payload["options"])
|
|
11126
|
+
question_rebuild_required = True
|
|
10859
11127
|
if "target_app_key" in payload:
|
|
10860
11128
|
field["target_app_key"] = payload["target_app_key"]
|
|
11129
|
+
question_rebuild_required = True
|
|
10861
11130
|
if "display_field" in payload:
|
|
10862
11131
|
field["display_field"] = payload["display_field"]
|
|
11132
|
+
question_rebuild_required = True
|
|
10863
11133
|
if "visible_fields" in payload:
|
|
10864
11134
|
field["visible_fields"] = list(payload["visible_fields"])
|
|
11135
|
+
question_rebuild_required = True
|
|
10865
11136
|
if "relation_mode" in payload:
|
|
10866
11137
|
field["relation_mode"] = payload["relation_mode"]
|
|
11138
|
+
question_rebuild_required = True
|
|
10867
11139
|
if "department_scope" in payload:
|
|
10868
11140
|
field["department_scope"] = payload["department_scope"]
|
|
11141
|
+
question_rebuild_required = True
|
|
10869
11142
|
if "remote_lookup_config" in payload:
|
|
10870
11143
|
field["remote_lookup_config"] = payload["remote_lookup_config"]
|
|
10871
11144
|
field["config"] = deepcopy(payload["remote_lookup_config"])
|
|
10872
11145
|
field["_explicit_remote_lookup_config"] = True
|
|
11146
|
+
question_rebuild_required = True
|
|
10873
11147
|
if "q_linker_binding" in payload:
|
|
10874
11148
|
field["q_linker_binding"] = payload["q_linker_binding"]
|
|
10875
11149
|
if "remote_lookup_config" not in payload:
|
|
10876
11150
|
field["_explicit_remote_lookup_config"] = False
|
|
11151
|
+
question_rebuild_required = True
|
|
10877
11152
|
if "code_block_config" in payload:
|
|
10878
11153
|
field["code_block_config"] = payload["code_block_config"]
|
|
10879
11154
|
field["config"] = deepcopy(payload["code_block_config"])
|
|
11155
|
+
question_rebuild_required = True
|
|
10880
11156
|
if "code_block_binding" in payload:
|
|
10881
11157
|
field["code_block_binding"] = payload["code_block_binding"]
|
|
10882
11158
|
field["_explicit_code_block_binding"] = True
|
|
11159
|
+
question_rebuild_required = True
|
|
10883
11160
|
if "auto_trigger" in payload:
|
|
10884
11161
|
field["auto_trigger"] = payload["auto_trigger"]
|
|
11162
|
+
question_rebuild_required = True
|
|
10885
11163
|
if "custom_button_text_enabled" in payload:
|
|
10886
11164
|
field["custom_button_text_enabled"] = payload["custom_button_text_enabled"]
|
|
11165
|
+
question_rebuild_required = True
|
|
10887
11166
|
if "custom_button_text" in payload:
|
|
10888
11167
|
field["custom_button_text"] = payload["custom_button_text"]
|
|
11168
|
+
question_rebuild_required = True
|
|
10889
11169
|
if "subfields" in payload:
|
|
10890
11170
|
field["subfields"] = [_field_patch_to_internal(item) for item in payload["subfields"]]
|
|
11171
|
+
question_rebuild_required = True
|
|
11172
|
+
if "subfield_updates" in payload:
|
|
11173
|
+
_apply_subfield_updates(field, payload["subfield_updates"])
|
|
10891
11174
|
if relation_config_explicit:
|
|
10892
11175
|
field["_relation_config_explicit"] = True
|
|
11176
|
+
question_rebuild_required = True
|
|
10893
11177
|
elif payload.get("type") and payload.get("type") != FieldType.relation.value:
|
|
10894
11178
|
field.pop("_relation_config_explicit", None)
|
|
10895
11179
|
field.pop("_reference_config_template", None)
|
|
11180
|
+
if question_overlay_keys:
|
|
11181
|
+
field["_question_overlay_keys"] = sorted(question_overlay_keys)
|
|
11182
|
+
else:
|
|
11183
|
+
field.pop("_question_overlay_keys", None)
|
|
11184
|
+
if question_rebuild_required:
|
|
11185
|
+
field["_question_rebuild_required"] = True
|
|
11186
|
+
else:
|
|
11187
|
+
field.pop("_question_rebuild_required", None)
|
|
10896
11188
|
|
|
10897
11189
|
|
|
10898
11190
|
def _resolve_field_selector_with_uniqueness(
|
|
@@ -12080,6 +12372,7 @@ def _warnings_from_verification_hints(hints: list[str]) -> list[dict[str, Any]]:
|
|
|
12080
12372
|
"package attachment not verified": _warning("PACKAGE_ATTACHMENT_UNVERIFIED", "package attachment is not verified"),
|
|
12081
12373
|
"layout has unplaced fields": _warning("LAYOUT_HAS_UNPLACED_FIELDS", "layout still contains unplaced fields"),
|
|
12082
12374
|
"no public views detected": _warning("NO_PUBLIC_VIEWS", "no public views were detected"),
|
|
12375
|
+
"schema_read_unavailable": _warning("SCHEMA_READ_UNAVAILABLE", "schema summary readback is unavailable"),
|
|
12083
12376
|
"views_read_unavailable": _warning("VIEWS_READ_UNAVAILABLE", "views summary readback is unavailable"),
|
|
12084
12377
|
"workflow_read_unavailable": _warning("WORKFLOW_READ_UNAVAILABLE", "workflow summary readback is unavailable"),
|
|
12085
12378
|
}
|
|
@@ -12380,6 +12673,26 @@ def _public_package_items_from_tag_items(tag_items: Any) -> list[JSONObject]:
|
|
|
12380
12673
|
return public_items
|
|
12381
12674
|
|
|
12382
12675
|
|
|
12676
|
+
def _select_package_layout_tag_items(*, detail: Any, base: Any) -> list[Any] | None:
|
|
12677
|
+
base_tag_items = base.get("tagItems") if isinstance(base, dict) and isinstance(base.get("tagItems"), list) else None
|
|
12678
|
+
detail_tag_items = detail.get("tagItems") if isinstance(detail, dict) and isinstance(detail.get("tagItems"), list) else None
|
|
12679
|
+
if _package_tag_items_include_groups(base_tag_items):
|
|
12680
|
+
return deepcopy(base_tag_items)
|
|
12681
|
+
if _package_tag_items_include_groups(detail_tag_items):
|
|
12682
|
+
return deepcopy(detail_tag_items)
|
|
12683
|
+
if detail_tag_items is not None:
|
|
12684
|
+
return deepcopy(detail_tag_items)
|
|
12685
|
+
if base_tag_items is not None:
|
|
12686
|
+
return deepcopy(base_tag_items)
|
|
12687
|
+
return None
|
|
12688
|
+
|
|
12689
|
+
|
|
12690
|
+
def _package_tag_items_include_groups(tag_items: Any) -> bool:
|
|
12691
|
+
if not isinstance(tag_items, list):
|
|
12692
|
+
return False
|
|
12693
|
+
return any(isinstance(item, dict) and _coerce_positive_int(item.get("itemType")) == 3 for item in tag_items)
|
|
12694
|
+
|
|
12695
|
+
|
|
12383
12696
|
def _flatten_package_resource_identities(items: Any, *, public: bool) -> set[tuple[str, str]]:
|
|
12384
12697
|
flattened: set[tuple[str, str]] = set()
|
|
12385
12698
|
|
|
@@ -12690,8 +13003,887 @@ def _verify_package_attachment(packages: PackageTools, *, profile: str, tag_id:
|
|
|
12690
13003
|
return last_result
|
|
12691
13004
|
|
|
12692
13005
|
|
|
13006
|
+
def _field_question_overlay_keys(field: dict[str, Any]) -> set[str]:
|
|
13007
|
+
raw_value = field.get("_question_overlay_keys")
|
|
13008
|
+
if isinstance(raw_value, set):
|
|
13009
|
+
return {str(item) for item in raw_value if isinstance(item, str) and item}
|
|
13010
|
+
if isinstance(raw_value, list):
|
|
13011
|
+
return {str(item) for item in raw_value if isinstance(item, str) and item}
|
|
13012
|
+
return set()
|
|
13013
|
+
|
|
13014
|
+
|
|
13015
|
+
def _field_needs_question_rebuild(field: dict[str, Any]) -> bool:
|
|
13016
|
+
return not isinstance(field.get("_question_template"), dict) or bool(field.get("_question_rebuild_required"))
|
|
13017
|
+
|
|
13018
|
+
|
|
13019
|
+
def _extract_template_row_lengths(schema: dict[str, Any]) -> tuple[dict[int, int], dict[str, int]]:
|
|
13020
|
+
lengths_by_que_id: dict[int, int] = {}
|
|
13021
|
+
lengths_by_title: dict[str, int] = {}
|
|
13022
|
+
|
|
13023
|
+
def remember_row(row: Any) -> None:
|
|
13024
|
+
if not isinstance(row, list):
|
|
13025
|
+
return
|
|
13026
|
+
questions = [question for question in row if isinstance(question, dict)]
|
|
13027
|
+
row_length = len(questions)
|
|
13028
|
+
if row_length <= 0:
|
|
13029
|
+
return
|
|
13030
|
+
for question in questions:
|
|
13031
|
+
que_id = _coerce_nonnegative_int(question.get("queId"))
|
|
13032
|
+
if que_id is not None:
|
|
13033
|
+
lengths_by_que_id[que_id] = row_length
|
|
13034
|
+
title = str(question.get("queTitle") or "").strip()
|
|
13035
|
+
if title:
|
|
13036
|
+
lengths_by_title[title] = row_length
|
|
13037
|
+
|
|
13038
|
+
for row in schema.get("formQues", []) or []:
|
|
13039
|
+
if not isinstance(row, list):
|
|
13040
|
+
continue
|
|
13041
|
+
if len(row) == 1 and isinstance(row[0], dict) and _coerce_positive_int(row[0].get("queType")) == 24:
|
|
13042
|
+
for inner_row in row[0].get("innerQuestions", []) or []:
|
|
13043
|
+
remember_row(inner_row)
|
|
13044
|
+
continue
|
|
13045
|
+
remember_row(row)
|
|
13046
|
+
return lengths_by_que_id, lengths_by_title
|
|
13047
|
+
|
|
13048
|
+
|
|
13049
|
+
def _field_template_row_length(
|
|
13050
|
+
field: dict[str, Any],
|
|
13051
|
+
*,
|
|
13052
|
+
lengths_by_que_id: dict[int, int],
|
|
13053
|
+
lengths_by_title: dict[str, int],
|
|
13054
|
+
) -> int | None:
|
|
13055
|
+
que_id = _coerce_nonnegative_int(field.get("que_id"))
|
|
13056
|
+
if que_id is not None and que_id in lengths_by_que_id:
|
|
13057
|
+
return lengths_by_que_id[que_id]
|
|
13058
|
+
template = field.get("_question_template")
|
|
13059
|
+
if isinstance(template, dict):
|
|
13060
|
+
template_que_id = _coerce_nonnegative_int(template.get("queId"))
|
|
13061
|
+
if template_que_id is not None and template_que_id in lengths_by_que_id:
|
|
13062
|
+
return lengths_by_que_id[template_que_id]
|
|
13063
|
+
template_title = str(template.get("queTitle") or "").strip()
|
|
13064
|
+
if template_title and template_title in lengths_by_title:
|
|
13065
|
+
return lengths_by_title[template_title]
|
|
13066
|
+
field_name = str(field.get("name") or "").strip()
|
|
13067
|
+
if field_name and field_name in lengths_by_title:
|
|
13068
|
+
return lengths_by_title[field_name]
|
|
13069
|
+
return None
|
|
13070
|
+
|
|
13071
|
+
|
|
13072
|
+
def _row_needs_width_reflow(expected_template_lengths: list[int], current_row_length: int) -> bool:
|
|
13073
|
+
if current_row_length <= 0:
|
|
13074
|
+
return False
|
|
13075
|
+
return any(length != current_row_length for length in expected_template_lengths)
|
|
13076
|
+
|
|
13077
|
+
|
|
13078
|
+
_FORM_SAVE_BASE_KEYS = (
|
|
13079
|
+
"formDesc",
|
|
13080
|
+
"formTheme",
|
|
13081
|
+
"formAttach",
|
|
13082
|
+
"formStyle",
|
|
13083
|
+
"serialNumType",
|
|
13084
|
+
"serialNumConfig",
|
|
13085
|
+
"attachVisibleOnlyConfig",
|
|
13086
|
+
"externalLang",
|
|
13087
|
+
"hideCopyright",
|
|
13088
|
+
)
|
|
13089
|
+
|
|
13090
|
+
_QUESTION_RELATION_SAVE_KEYS = (
|
|
13091
|
+
"queId",
|
|
13092
|
+
"relationType",
|
|
13093
|
+
"displayedQueId",
|
|
13094
|
+
"qlinkerAlias",
|
|
13095
|
+
"displayedQueInfo",
|
|
13096
|
+
"aliasConfig",
|
|
13097
|
+
"matchRules",
|
|
13098
|
+
"tableMatchRules",
|
|
13099
|
+
"matchRuleType",
|
|
13100
|
+
"matchRuleFormula",
|
|
13101
|
+
"sortConfig",
|
|
13102
|
+
)
|
|
13103
|
+
|
|
13104
|
+
_RELATION_QUESTION_SAVE_KEYS = (
|
|
13105
|
+
"queId",
|
|
13106
|
+
"queTempId",
|
|
13107
|
+
"queType",
|
|
13108
|
+
"queOriginType",
|
|
13109
|
+
"queTitle",
|
|
13110
|
+
"queWidth",
|
|
13111
|
+
"scanType",
|
|
13112
|
+
"status",
|
|
13113
|
+
"required",
|
|
13114
|
+
"queHint",
|
|
13115
|
+
"linkedQuestions",
|
|
13116
|
+
"logicalShow",
|
|
13117
|
+
"queDefaultType",
|
|
13118
|
+
"queDefaultValue",
|
|
13119
|
+
"queDefaultValues",
|
|
13120
|
+
"subQueWidth",
|
|
13121
|
+
"innerQuestions",
|
|
13122
|
+
"minOpts",
|
|
13123
|
+
"maxOpts",
|
|
13124
|
+
"beingHide",
|
|
13125
|
+
"beingDesensitized",
|
|
13126
|
+
"relationDisplayMode",
|
|
13127
|
+
"customRenderConfig",
|
|
13128
|
+
)
|
|
13129
|
+
|
|
13130
|
+
_REFERENCE_CONFIG_SAVE_KEYS = (
|
|
13131
|
+
"referAppKey",
|
|
13132
|
+
"referQueId",
|
|
13133
|
+
"customButtonText",
|
|
13134
|
+
"beingTableSource",
|
|
13135
|
+
"referMatchRules",
|
|
13136
|
+
"canAddData",
|
|
13137
|
+
"dataAdditionButtonText",
|
|
13138
|
+
"canViewProcessLog",
|
|
13139
|
+
"optionalDataNum",
|
|
13140
|
+
"beingDataLogVisible",
|
|
13141
|
+
"beingDefaultFormulaAutoFillEnabled",
|
|
13142
|
+
"defaultValueMatchRules",
|
|
13143
|
+
"configShowForm",
|
|
13144
|
+
"configSortFieldId",
|
|
13145
|
+
"configAsc",
|
|
13146
|
+
"dataShowForm",
|
|
13147
|
+
"defaultRow",
|
|
13148
|
+
"fieldNameShow",
|
|
13149
|
+
"dataSortFieldId",
|
|
13150
|
+
"dataSortAsc",
|
|
13151
|
+
)
|
|
13152
|
+
|
|
13153
|
+
_REFERENCE_QUESTION_SAVE_KEYS = (
|
|
13154
|
+
"queId",
|
|
13155
|
+
"queTitle",
|
|
13156
|
+
"queType",
|
|
13157
|
+
"queAuth",
|
|
13158
|
+
"ordinal",
|
|
13159
|
+
)
|
|
13160
|
+
|
|
13161
|
+
_REFERENCE_FILL_RULE_SAVE_KEYS = (
|
|
13162
|
+
"queId",
|
|
13163
|
+
"relatedQueId",
|
|
13164
|
+
"queTitle",
|
|
13165
|
+
"relatedQueTitle",
|
|
13166
|
+
)
|
|
13167
|
+
|
|
13168
|
+
_REFERENCE_AUTH_QUESTION_SAVE_KEYS = (
|
|
13169
|
+
"queId",
|
|
13170
|
+
"queAuth",
|
|
13171
|
+
)
|
|
13172
|
+
|
|
13173
|
+
|
|
13174
|
+
def _copy_present_keys(
|
|
13175
|
+
source: dict[str, Any],
|
|
13176
|
+
keys: tuple[str, ...],
|
|
13177
|
+
*,
|
|
13178
|
+
keep_none_keys: tuple[str, ...] = (),
|
|
13179
|
+
) -> dict[str, Any]:
|
|
13180
|
+
payload: dict[str, Any] = {}
|
|
13181
|
+
keep_none = set(keep_none_keys)
|
|
13182
|
+
for key in keys:
|
|
13183
|
+
if key not in source:
|
|
13184
|
+
continue
|
|
13185
|
+
value = source.get(key)
|
|
13186
|
+
if value is None and key not in keep_none:
|
|
13187
|
+
continue
|
|
13188
|
+
payload[key] = deepcopy(value)
|
|
13189
|
+
return payload
|
|
13190
|
+
|
|
13191
|
+
|
|
13192
|
+
def _looks_like_backend_encoded_formula(value: str) -> bool:
|
|
13193
|
+
if len(value) <= 32:
|
|
13194
|
+
return False
|
|
13195
|
+
encoded = value[16:-16]
|
|
13196
|
+
if not encoded:
|
|
13197
|
+
return False
|
|
13198
|
+
try:
|
|
13199
|
+
decoded = base64.b64decode(encoded, validate=True).decode("utf-8")
|
|
13200
|
+
unquote_plus(decoded)
|
|
13201
|
+
except Exception:
|
|
13202
|
+
return False
|
|
13203
|
+
return True
|
|
13204
|
+
|
|
13205
|
+
|
|
13206
|
+
def _encode_formula_for_backend_save(value: Any) -> Any:
|
|
13207
|
+
if not isinstance(value, str) or not value:
|
|
13208
|
+
return value
|
|
13209
|
+
if _looks_like_backend_encoded_formula(value):
|
|
13210
|
+
return value
|
|
13211
|
+
encoded = quote_plus(value, encoding="utf-8")
|
|
13212
|
+
b64_value = base64.b64encode(encoded.encode("utf-8")).decode("ascii")
|
|
13213
|
+
alphabet = string.ascii_letters + string.digits
|
|
13214
|
+
prefix = "".join(random.choice(alphabet) for _ in range(16))
|
|
13215
|
+
suffix = "".join(random.choice(alphabet) for _ in range(16))
|
|
13216
|
+
return f"{prefix}{b64_value}{suffix}"
|
|
13217
|
+
|
|
13218
|
+
|
|
13219
|
+
def _normalize_formula_defaults_for_save(value: Any) -> None:
|
|
13220
|
+
if isinstance(value, list):
|
|
13221
|
+
for item in value:
|
|
13222
|
+
_normalize_formula_defaults_for_save(item)
|
|
13223
|
+
return
|
|
13224
|
+
if not isinstance(value, dict):
|
|
13225
|
+
return
|
|
13226
|
+
if _coerce_any_int(value.get("queDefaultType")) == DEFAULT_TYPE_FORMULA and value.get("queDefaultValue"):
|
|
13227
|
+
value["queDefaultValue"] = _encode_formula_for_backend_save(value.get("queDefaultValue"))
|
|
13228
|
+
for key in ("subQuestions", "innerQuestions"):
|
|
13229
|
+
nested = value.get(key)
|
|
13230
|
+
if isinstance(nested, (list, dict)):
|
|
13231
|
+
_normalize_formula_defaults_for_save(nested)
|
|
13232
|
+
|
|
13233
|
+
|
|
13234
|
+
def _normalize_reference_question_for_save(value: Any, *, ordinal: int) -> dict[str, Any] | None:
|
|
13235
|
+
if not isinstance(value, dict):
|
|
13236
|
+
return None
|
|
13237
|
+
payload = _copy_present_keys(value, _REFERENCE_QUESTION_SAVE_KEYS)
|
|
13238
|
+
que_id = _coerce_any_int(value.get("queId"))
|
|
13239
|
+
if que_id is not None:
|
|
13240
|
+
payload["queId"] = que_id
|
|
13241
|
+
if "ordinal" not in payload:
|
|
13242
|
+
payload["ordinal"] = _coerce_nonnegative_int(value.get("ordinal"))
|
|
13243
|
+
if payload.get("ordinal") is None:
|
|
13244
|
+
payload["ordinal"] = ordinal
|
|
13245
|
+
if not any(key in payload for key in ("queId", "queTitle", "queType")):
|
|
13246
|
+
return None
|
|
13247
|
+
return payload
|
|
13248
|
+
|
|
13249
|
+
|
|
13250
|
+
def _normalize_reference_fill_rule_for_save(value: Any) -> dict[str, Any] | None:
|
|
13251
|
+
if not isinstance(value, dict):
|
|
13252
|
+
return None
|
|
13253
|
+
payload = _copy_present_keys(value, _REFERENCE_FILL_RULE_SAVE_KEYS)
|
|
13254
|
+
que_id = _coerce_nonnegative_int(value.get("queId"))
|
|
13255
|
+
related_que_id = _coerce_nonnegative_int(value.get("relatedQueId", value.get("referQueId")))
|
|
13256
|
+
if que_id is not None:
|
|
13257
|
+
payload["queId"] = que_id
|
|
13258
|
+
if related_que_id is not None:
|
|
13259
|
+
payload["relatedQueId"] = related_que_id
|
|
13260
|
+
if "relatedQueTitle" not in payload and value.get("referQueTitle") is not None:
|
|
13261
|
+
payload["relatedQueTitle"] = str(value.get("referQueTitle") or "")
|
|
13262
|
+
if "queId" not in payload or "relatedQueId" not in payload:
|
|
13263
|
+
return None
|
|
13264
|
+
return payload
|
|
13265
|
+
|
|
13266
|
+
|
|
13267
|
+
def _normalize_reference_auth_question_for_save(value: Any) -> dict[str, Any] | None:
|
|
13268
|
+
if not isinstance(value, dict):
|
|
13269
|
+
return None
|
|
13270
|
+
payload = _copy_present_keys(value, _REFERENCE_AUTH_QUESTION_SAVE_KEYS)
|
|
13271
|
+
que_id = _coerce_any_int(value.get("queId"))
|
|
13272
|
+
if que_id is not None:
|
|
13273
|
+
payload["queId"] = que_id
|
|
13274
|
+
que_auth = _coerce_nonnegative_int(value.get("queAuth"))
|
|
13275
|
+
if que_auth is not None:
|
|
13276
|
+
payload["queAuth"] = que_auth
|
|
13277
|
+
sub_ques = [
|
|
13278
|
+
item
|
|
13279
|
+
for item in (
|
|
13280
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13281
|
+
for raw_item in cast(list[Any], value.get("subQues") or [])
|
|
13282
|
+
)
|
|
13283
|
+
if item is not None
|
|
13284
|
+
]
|
|
13285
|
+
inner_ques = [
|
|
13286
|
+
item
|
|
13287
|
+
for item in (
|
|
13288
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13289
|
+
for raw_item in cast(list[Any], value.get("innerQues") or [])
|
|
13290
|
+
)
|
|
13291
|
+
if item is not None
|
|
13292
|
+
]
|
|
13293
|
+
if sub_ques or "subQues" in value:
|
|
13294
|
+
payload["subQues"] = sub_ques
|
|
13295
|
+
if inner_ques or "innerQues" in value:
|
|
13296
|
+
payload["innerQues"] = inner_ques
|
|
13297
|
+
if "queId" not in payload or "queAuth" not in payload:
|
|
13298
|
+
return None
|
|
13299
|
+
return payload
|
|
13300
|
+
|
|
13301
|
+
|
|
13302
|
+
def _dedupe_reference_auth_questions(auth_questions: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
13303
|
+
deduped: list[dict[str, Any]] = []
|
|
13304
|
+
seen_que_ids: set[int] = set()
|
|
13305
|
+
for item in auth_questions:
|
|
13306
|
+
normalized_item = _normalize_reference_auth_question_for_save(item)
|
|
13307
|
+
if normalized_item is None:
|
|
13308
|
+
continue
|
|
13309
|
+
que_id = _coerce_any_int(normalized_item.get("queId"))
|
|
13310
|
+
if que_id is None or que_id in seen_que_ids:
|
|
13311
|
+
continue
|
|
13312
|
+
seen_que_ids.add(que_id)
|
|
13313
|
+
deduped.append(normalized_item)
|
|
13314
|
+
return deduped
|
|
13315
|
+
|
|
13316
|
+
|
|
13317
|
+
_REFERENCE_FIELD_HIDDEN_AUTH = 2
|
|
13318
|
+
_REFERENCE_FIELD_VISIBLE_AUTH = 3
|
|
13319
|
+
|
|
13320
|
+
|
|
13321
|
+
def _synthesize_reference_auth_questions_for_save(
|
|
13322
|
+
*,
|
|
13323
|
+
source: dict[str, Any],
|
|
13324
|
+
field: dict[str, Any],
|
|
13325
|
+
) -> list[dict[str, Any]]:
|
|
13326
|
+
config = field.get("config") if isinstance(field.get("config"), dict) else {}
|
|
13327
|
+
synthesized: list[dict[str, Any]] = []
|
|
13328
|
+
|
|
13329
|
+
if isinstance(config.get("refer_auth_ques"), list):
|
|
13330
|
+
synthesized.extend(cast(list[dict[str, Any]], config.get("refer_auth_ques") or []))
|
|
13331
|
+
if synthesized:
|
|
13332
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13333
|
+
|
|
13334
|
+
refer_question_ids_by_name: dict[str, int] = {}
|
|
13335
|
+
for raw_item in cast(list[Any], source.get("referQuestions") or []):
|
|
13336
|
+
if not isinstance(raw_item, dict):
|
|
13337
|
+
continue
|
|
13338
|
+
que_id = _coerce_any_int(raw_item.get("queId"))
|
|
13339
|
+
name = str(raw_item.get("queTitle") or "").strip()
|
|
13340
|
+
if que_id is None or not name or name in refer_question_ids_by_name:
|
|
13341
|
+
continue
|
|
13342
|
+
refer_question_ids_by_name[name] = que_id
|
|
13343
|
+
|
|
13344
|
+
visible_fields = cast(list[dict[str, Any]], field.get("visible_fields") or [])
|
|
13345
|
+
for item in visible_fields:
|
|
13346
|
+
if not isinstance(item, dict):
|
|
13347
|
+
continue
|
|
13348
|
+
que_id = _coerce_any_int(item.get("que_id"))
|
|
13349
|
+
if que_id is None:
|
|
13350
|
+
name = str(item.get("name") or "").strip()
|
|
13351
|
+
que_id = refer_question_ids_by_name.get(name)
|
|
13352
|
+
if que_id is None:
|
|
13353
|
+
continue
|
|
13354
|
+
synthesized.append({"queId": que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
|
|
13355
|
+
if synthesized:
|
|
13356
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13357
|
+
|
|
13358
|
+
auth_field_que_ids = cast(list[Any], config.get("auth_field_que_ids") or [])
|
|
13359
|
+
for raw_que_id in auth_field_que_ids:
|
|
13360
|
+
que_id = _coerce_any_int(raw_que_id)
|
|
13361
|
+
if que_id is None:
|
|
13362
|
+
continue
|
|
13363
|
+
synthesized.append({"queId": que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
|
|
13364
|
+
if synthesized:
|
|
13365
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13366
|
+
|
|
13367
|
+
for raw_item in cast(list[Any], source.get("referQuestions") or []):
|
|
13368
|
+
if not isinstance(raw_item, dict):
|
|
13369
|
+
continue
|
|
13370
|
+
que_id = _coerce_any_int(raw_item.get("queId"))
|
|
13371
|
+
if que_id is None:
|
|
13372
|
+
continue
|
|
13373
|
+
synthesized.append(
|
|
13374
|
+
{
|
|
13375
|
+
"queId": que_id,
|
|
13376
|
+
"queAuth": _REFERENCE_FIELD_VISIBLE_AUTH,
|
|
13377
|
+
}
|
|
13378
|
+
)
|
|
13379
|
+
if synthesized:
|
|
13380
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13381
|
+
|
|
13382
|
+
fallback_que_id = _coerce_any_int(field.get("target_field_que_id"))
|
|
13383
|
+
if fallback_que_id is not None:
|
|
13384
|
+
synthesized.append({"queId": fallback_que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
|
|
13385
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13386
|
+
|
|
13387
|
+
|
|
13388
|
+
def _reference_question_auth_overrides_for_save(
|
|
13389
|
+
*,
|
|
13390
|
+
source: dict[str, Any],
|
|
13391
|
+
field: dict[str, Any],
|
|
13392
|
+
) -> dict[int, int]:
|
|
13393
|
+
overrides: dict[int, int] = {}
|
|
13394
|
+
visible_que_ids: set[int] = set()
|
|
13395
|
+
|
|
13396
|
+
for item in cast(list[Any], field.get("visible_fields") or []):
|
|
13397
|
+
if not isinstance(item, dict):
|
|
13398
|
+
continue
|
|
13399
|
+
que_id = _coerce_any_int(item.get("que_id"))
|
|
13400
|
+
if que_id is not None:
|
|
13401
|
+
visible_que_ids.add(que_id)
|
|
13402
|
+
|
|
13403
|
+
if not visible_que_ids:
|
|
13404
|
+
refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
|
|
13405
|
+
for item in refer_auth_ques:
|
|
13406
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13407
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13408
|
+
if que_id is None or que_auth is None:
|
|
13409
|
+
continue
|
|
13410
|
+
overrides[que_id] = que_auth
|
|
13411
|
+
if overrides:
|
|
13412
|
+
return overrides
|
|
13413
|
+
|
|
13414
|
+
for raw_item in cast(list[Any], source.get("referQuestions") or []):
|
|
13415
|
+
if not isinstance(raw_item, dict):
|
|
13416
|
+
continue
|
|
13417
|
+
que_id = _coerce_any_int(raw_item.get("queId"))
|
|
13418
|
+
if que_id is None:
|
|
13419
|
+
continue
|
|
13420
|
+
overrides[que_id] = (
|
|
13421
|
+
_REFERENCE_FIELD_VISIBLE_AUTH if que_id in visible_que_ids else _REFERENCE_FIELD_HIDDEN_AUTH
|
|
13422
|
+
)
|
|
13423
|
+
return overrides
|
|
13424
|
+
|
|
13425
|
+
|
|
13426
|
+
def _reference_question_matches_visible_selector(question: dict[str, Any], selector: dict[str, Any]) -> bool:
|
|
13427
|
+
question_que_id = _coerce_any_int(question.get("queId"))
|
|
13428
|
+
selector_que_id = _coerce_any_int(selector.get("que_id"))
|
|
13429
|
+
if question_que_id is not None and selector_que_id is not None and question_que_id == selector_que_id:
|
|
13430
|
+
return True
|
|
13431
|
+
question_name = str(question.get("queTitle") or "").strip()
|
|
13432
|
+
selector_name = str(selector.get("name") or "").strip()
|
|
13433
|
+
return bool(question_name and selector_name and question_name == selector_name)
|
|
13434
|
+
|
|
13435
|
+
|
|
13436
|
+
def _build_reference_question_from_visible_selector(
|
|
13437
|
+
selector: dict[str, Any],
|
|
13438
|
+
*,
|
|
13439
|
+
ordinal: int,
|
|
13440
|
+
) -> dict[str, Any] | None:
|
|
13441
|
+
return _normalize_reference_question_for_save(
|
|
13442
|
+
{
|
|
13443
|
+
"queId": _coerce_any_int(selector.get("que_id")),
|
|
13444
|
+
"queTitle": str(selector.get("name") or "").strip() or None,
|
|
13445
|
+
"queType": str(selector.get("type") or "2"),
|
|
13446
|
+
"ordinal": ordinal,
|
|
13447
|
+
},
|
|
13448
|
+
ordinal=ordinal,
|
|
13449
|
+
)
|
|
13450
|
+
|
|
13451
|
+
|
|
13452
|
+
def _canonicalize_reference_questions_for_save(
|
|
13453
|
+
*,
|
|
13454
|
+
source: dict[str, Any],
|
|
13455
|
+
field: dict[str, Any],
|
|
13456
|
+
) -> list[dict[str, Any]]:
|
|
13457
|
+
relation_config_explicit = bool(field.get("_relation_config_explicit"))
|
|
13458
|
+
normalized_source_questions = [
|
|
13459
|
+
item
|
|
13460
|
+
for item in (
|
|
13461
|
+
_normalize_reference_question_for_save(raw_item, ordinal=index)
|
|
13462
|
+
for index, raw_item in enumerate(cast(list[Any], source.get("referQuestions") or []), start=1)
|
|
13463
|
+
)
|
|
13464
|
+
if item is not None
|
|
13465
|
+
]
|
|
13466
|
+
if not relation_config_explicit:
|
|
13467
|
+
return normalized_source_questions
|
|
13468
|
+
|
|
13469
|
+
display_field = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
|
|
13470
|
+
visible_fields = [item for item in cast(list[Any], field.get("visible_fields") or []) if isinstance(item, dict)]
|
|
13471
|
+
ordered_visible_selectors: list[dict[str, Any]] = []
|
|
13472
|
+
if display_field is not None:
|
|
13473
|
+
ordered_visible_selectors.append(display_field)
|
|
13474
|
+
for item in visible_fields:
|
|
13475
|
+
if any(_relation_target_field_matches(existing, item) for existing in ordered_visible_selectors):
|
|
13476
|
+
continue
|
|
13477
|
+
ordered_visible_selectors.append(item)
|
|
13478
|
+
|
|
13479
|
+
if not ordered_visible_selectors:
|
|
13480
|
+
return normalized_source_questions
|
|
13481
|
+
|
|
13482
|
+
canonical_questions: list[dict[str, Any]] = []
|
|
13483
|
+
used_source_indexes: set[int] = set()
|
|
13484
|
+
|
|
13485
|
+
for ordinal, selector in enumerate(ordered_visible_selectors, start=1):
|
|
13486
|
+
matched_index: int | None = None
|
|
13487
|
+
matched_item: dict[str, Any] | None = None
|
|
13488
|
+
for index, item in enumerate(normalized_source_questions):
|
|
13489
|
+
if index in used_source_indexes:
|
|
13490
|
+
continue
|
|
13491
|
+
if _reference_question_matches_visible_selector(item, selector):
|
|
13492
|
+
matched_index = index
|
|
13493
|
+
matched_item = deepcopy(item)
|
|
13494
|
+
break
|
|
13495
|
+
if matched_item is None:
|
|
13496
|
+
matched_item = _build_reference_question_from_visible_selector(selector, ordinal=ordinal)
|
|
13497
|
+
if matched_item is None:
|
|
13498
|
+
continue
|
|
13499
|
+
matched_item["ordinal"] = ordinal
|
|
13500
|
+
canonical_questions.append(matched_item)
|
|
13501
|
+
if matched_index is not None:
|
|
13502
|
+
used_source_indexes.add(matched_index)
|
|
13503
|
+
|
|
13504
|
+
source_target_app_key = str(source.get("referAppKey") or "").strip()
|
|
13505
|
+
target_app_key = str(field.get("target_app_key") or "").strip()
|
|
13506
|
+
preserve_remaining_source_questions = not source_target_app_key or source_target_app_key == target_app_key
|
|
13507
|
+
|
|
13508
|
+
if preserve_remaining_source_questions:
|
|
13509
|
+
next_ordinal = len(canonical_questions) + 1
|
|
13510
|
+
for index, item in enumerate(normalized_source_questions):
|
|
13511
|
+
if index in used_source_indexes:
|
|
13512
|
+
continue
|
|
13513
|
+
remaining_item = deepcopy(item)
|
|
13514
|
+
remaining_item["ordinal"] = next_ordinal
|
|
13515
|
+
next_ordinal += 1
|
|
13516
|
+
canonical_questions.append(remaining_item)
|
|
13517
|
+
|
|
13518
|
+
return canonical_questions
|
|
13519
|
+
|
|
13520
|
+
|
|
13521
|
+
def _canonicalize_reference_auth_questions_for_save(
|
|
13522
|
+
*,
|
|
13523
|
+
source: dict[str, Any],
|
|
13524
|
+
refer_questions: list[dict[str, Any]],
|
|
13525
|
+
relation_config_explicit: bool,
|
|
13526
|
+
) -> list[dict[str, Any]]:
|
|
13527
|
+
source_auth_questions = [
|
|
13528
|
+
item
|
|
13529
|
+
for item in (
|
|
13530
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13531
|
+
for raw_item in cast(list[Any], source.get("referAuthQues") or [])
|
|
13532
|
+
)
|
|
13533
|
+
if item is not None
|
|
13534
|
+
]
|
|
13535
|
+
source_auth_by_que_id: dict[int, dict[str, Any]] = {}
|
|
13536
|
+
for item in source_auth_questions:
|
|
13537
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13538
|
+
if que_id is None or que_id in source_auth_by_que_id:
|
|
13539
|
+
continue
|
|
13540
|
+
source_auth_by_que_id[que_id] = item
|
|
13541
|
+
|
|
13542
|
+
if not relation_config_explicit:
|
|
13543
|
+
auth_questions: list[dict[str, Any]] = []
|
|
13544
|
+
seen_que_ids: set[int] = set()
|
|
13545
|
+
refer_question_auth_by_que_id: dict[int, int] = {}
|
|
13546
|
+
for item in refer_questions:
|
|
13547
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13548
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13549
|
+
if que_id is None or que_auth is None or que_id in refer_question_auth_by_que_id:
|
|
13550
|
+
continue
|
|
13551
|
+
refer_question_auth_by_que_id[que_id] = que_auth
|
|
13552
|
+
|
|
13553
|
+
for item in source_auth_questions:
|
|
13554
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13555
|
+
if que_id is None or que_id in seen_que_ids:
|
|
13556
|
+
continue
|
|
13557
|
+
payload = deepcopy(item)
|
|
13558
|
+
if que_id in refer_question_auth_by_que_id:
|
|
13559
|
+
payload["queAuth"] = refer_question_auth_by_que_id[que_id]
|
|
13560
|
+
auth_questions.append(payload)
|
|
13561
|
+
seen_que_ids.add(que_id)
|
|
13562
|
+
|
|
13563
|
+
for item in refer_questions:
|
|
13564
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13565
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13566
|
+
if que_id is None or que_auth is None or que_id in seen_que_ids:
|
|
13567
|
+
continue
|
|
13568
|
+
payload = deepcopy(source_auth_by_que_id.get(que_id) or {"queId": que_id})
|
|
13569
|
+
payload["queId"] = que_id
|
|
13570
|
+
payload["queAuth"] = que_auth
|
|
13571
|
+
auth_questions.append(payload)
|
|
13572
|
+
seen_que_ids.add(que_id)
|
|
13573
|
+
|
|
13574
|
+
return _dedupe_reference_auth_questions(auth_questions)
|
|
13575
|
+
|
|
13576
|
+
auth_questions: list[dict[str, Any]] = []
|
|
13577
|
+
for item in refer_questions:
|
|
13578
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13579
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13580
|
+
if que_id is None or que_auth is None:
|
|
13581
|
+
continue
|
|
13582
|
+
payload = deepcopy(source_auth_by_que_id.get(que_id) or {"queId": que_id})
|
|
13583
|
+
payload["queId"] = que_id
|
|
13584
|
+
payload["queAuth"] = que_auth
|
|
13585
|
+
auth_questions.append(payload)
|
|
13586
|
+
return _dedupe_reference_auth_questions(auth_questions)
|
|
13587
|
+
|
|
13588
|
+
|
|
13589
|
+
def _enforce_reference_config_consistency_for_save(
|
|
13590
|
+
payload: dict[str, Any],
|
|
13591
|
+
*,
|
|
13592
|
+
field: dict[str, Any],
|
|
13593
|
+
) -> dict[str, Any]:
|
|
13594
|
+
relation_config_explicit = bool(field.get("_relation_config_explicit"))
|
|
13595
|
+
refer_questions = [
|
|
13596
|
+
item
|
|
13597
|
+
for item in (
|
|
13598
|
+
_normalize_reference_question_for_save(raw_item, ordinal=index)
|
|
13599
|
+
for index, raw_item in enumerate(cast(list[Any], payload.get("referQuestions") or []), start=1)
|
|
13600
|
+
)
|
|
13601
|
+
if item is not None
|
|
13602
|
+
]
|
|
13603
|
+
if not refer_questions:
|
|
13604
|
+
return payload
|
|
13605
|
+
|
|
13606
|
+
refer_auth_ques = _dedupe_reference_auth_questions(
|
|
13607
|
+
[
|
|
13608
|
+
item
|
|
13609
|
+
for item in (
|
|
13610
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13611
|
+
for raw_item in cast(list[Any], payload.get("referAuthQues") or [])
|
|
13612
|
+
)
|
|
13613
|
+
if item is not None
|
|
13614
|
+
]
|
|
13615
|
+
)
|
|
13616
|
+
refer_auth_by_que_id: dict[int, int] = {}
|
|
13617
|
+
for item in refer_auth_ques:
|
|
13618
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13619
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13620
|
+
if que_id is None or que_auth is None or que_id in refer_auth_by_que_id:
|
|
13621
|
+
continue
|
|
13622
|
+
refer_auth_by_que_id[que_id] = que_auth
|
|
13623
|
+
|
|
13624
|
+
display_field_que_id = _coerce_any_int(payload.get("referQueId"))
|
|
13625
|
+
if display_field_que_id is None:
|
|
13626
|
+
display_field_que_id = _coerce_any_int(field.get("target_field_que_id"))
|
|
13627
|
+
if display_field_que_id is not None:
|
|
13628
|
+
payload["referQueId"] = display_field_que_id
|
|
13629
|
+
|
|
13630
|
+
if relation_config_explicit:
|
|
13631
|
+
if display_field_que_id is not None and not any(
|
|
13632
|
+
_coerce_any_int(item.get("queId")) == display_field_que_id for item in refer_questions
|
|
13633
|
+
):
|
|
13634
|
+
display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
|
|
13635
|
+
display_question = (
|
|
13636
|
+
_build_reference_question_from_visible_selector(display_selector, ordinal=1)
|
|
13637
|
+
if display_selector is not None
|
|
13638
|
+
else None
|
|
13639
|
+
)
|
|
13640
|
+
if display_question is not None:
|
|
13641
|
+
display_question["queId"] = display_field_que_id
|
|
13642
|
+
display_question["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
|
|
13643
|
+
refer_questions = [display_question, *refer_questions]
|
|
13644
|
+
|
|
13645
|
+
if display_field_que_id is not None:
|
|
13646
|
+
display_questions = [
|
|
13647
|
+
item for item in refer_questions if _coerce_any_int(item.get("queId")) == display_field_que_id
|
|
13648
|
+
]
|
|
13649
|
+
trailing_questions = [
|
|
13650
|
+
item for item in refer_questions if _coerce_any_int(item.get("queId")) != display_field_que_id
|
|
13651
|
+
]
|
|
13652
|
+
refer_questions = [*display_questions, *trailing_questions]
|
|
13653
|
+
|
|
13654
|
+
for ordinal, item in enumerate(refer_questions, start=1):
|
|
13655
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13656
|
+
if que_id is None:
|
|
13657
|
+
continue
|
|
13658
|
+
item["ordinal"] = ordinal
|
|
13659
|
+
item["queAuth"] = refer_auth_by_que_id.get(
|
|
13660
|
+
que_id,
|
|
13661
|
+
_coerce_nonnegative_int(item.get("queAuth")) or _REFERENCE_FIELD_VISIBLE_AUTH,
|
|
13662
|
+
)
|
|
13663
|
+
if display_field_que_id is not None and que_id == display_field_que_id:
|
|
13664
|
+
item["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
|
|
13665
|
+
|
|
13666
|
+
payload["referQuestions"] = refer_questions
|
|
13667
|
+
payload["referAuthQues"] = _canonicalize_reference_auth_questions_for_save(
|
|
13668
|
+
source={"referAuthQues": refer_auth_ques},
|
|
13669
|
+
refer_questions=refer_questions,
|
|
13670
|
+
relation_config_explicit=relation_config_explicit,
|
|
13671
|
+
)
|
|
13672
|
+
return payload
|
|
13673
|
+
|
|
13674
|
+
|
|
13675
|
+
def _normalize_reference_config_for_save(
|
|
13676
|
+
reference: Any,
|
|
13677
|
+
*,
|
|
13678
|
+
field: dict[str, Any],
|
|
13679
|
+
) -> dict[str, Any]:
|
|
13680
|
+
source = reference if isinstance(reference, dict) else {}
|
|
13681
|
+
payload = _copy_present_keys(source, _REFERENCE_CONFIG_SAVE_KEYS)
|
|
13682
|
+
if str(field.get("target_app_key") or "").strip():
|
|
13683
|
+
payload["referAppKey"] = str(field.get("target_app_key") or "").strip()
|
|
13684
|
+
if field.get("target_field_que_id") is not None:
|
|
13685
|
+
payload["referQueId"] = _coerce_nonnegative_int(field.get("target_field_que_id"))
|
|
13686
|
+
if field.get("field_name_show") is not None:
|
|
13687
|
+
payload["fieldNameShow"] = bool(field.get("field_name_show"))
|
|
13688
|
+
|
|
13689
|
+
refer_question_auth_overrides = _reference_question_auth_overrides_for_save(source=source, field=field)
|
|
13690
|
+
refer_questions = _canonicalize_reference_questions_for_save(source=source, field=field)
|
|
13691
|
+
for index, normalized_item in enumerate(refer_questions, start=1):
|
|
13692
|
+
que_id = _coerce_any_int(normalized_item.get("queId"))
|
|
13693
|
+
if que_id is not None and que_id in refer_question_auth_overrides:
|
|
13694
|
+
normalized_item["queAuth"] = refer_question_auth_overrides[que_id]
|
|
13695
|
+
normalized_item["ordinal"] = index
|
|
13696
|
+
if refer_questions or "referQuestions" in source:
|
|
13697
|
+
payload["referQuestions"] = refer_questions
|
|
13698
|
+
|
|
13699
|
+
refer_fill_rules = [
|
|
13700
|
+
item
|
|
13701
|
+
for item in (
|
|
13702
|
+
_normalize_reference_fill_rule_for_save(raw_item)
|
|
13703
|
+
for raw_item in cast(list[Any], source.get("referFillRules") or [])
|
|
13704
|
+
)
|
|
13705
|
+
if item is not None
|
|
13706
|
+
]
|
|
13707
|
+
if refer_fill_rules or "referFillRules" in source:
|
|
13708
|
+
payload["referFillRules"] = refer_fill_rules
|
|
13709
|
+
|
|
13710
|
+
refer_auth_ques = _canonicalize_reference_auth_questions_for_save(
|
|
13711
|
+
source=source,
|
|
13712
|
+
refer_questions=refer_questions,
|
|
13713
|
+
relation_config_explicit=bool(field.get("_relation_config_explicit")),
|
|
13714
|
+
)
|
|
13715
|
+
if not refer_auth_ques:
|
|
13716
|
+
refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
|
|
13717
|
+
if refer_auth_ques or "referAuthQues" in source:
|
|
13718
|
+
payload["referAuthQues"] = refer_auth_ques
|
|
13719
|
+
|
|
13720
|
+
return _enforce_reference_config_consistency_for_save(payload, field=field)
|
|
13721
|
+
|
|
13722
|
+
|
|
13723
|
+
def _normalize_relation_question_for_save(question: dict[str, Any], *, field: dict[str, Any]) -> dict[str, Any]:
|
|
13724
|
+
payload = _copy_present_keys(question, _RELATION_QUESTION_SAVE_KEYS)
|
|
13725
|
+
overlay_keys = _field_question_overlay_keys(field)
|
|
13726
|
+
que_id = _coerce_nonnegative_int(question.get("queId"))
|
|
13727
|
+
if que_id is not None:
|
|
13728
|
+
payload["queId"] = que_id
|
|
13729
|
+
que_temp_id = _coerce_nonnegative_int(question.get("queTempId"))
|
|
13730
|
+
if que_temp_id is not None and "queId" not in payload:
|
|
13731
|
+
payload["queTempId"] = que_temp_id
|
|
13732
|
+
payload["queType"] = _coerce_positive_int(question.get("queType")) or 25
|
|
13733
|
+
payload["queTitle"] = str(field.get("name") or question.get("queTitle") or "")
|
|
13734
|
+
if "required" in overlay_keys or "required" in question or field.get("required") is not None:
|
|
13735
|
+
payload["required"] = bool(field.get("required", question.get("required", False)))
|
|
13736
|
+
if "description" in overlay_keys:
|
|
13737
|
+
payload["queHint"] = "" if field.get("description") is None else str(field.get("description"))
|
|
13738
|
+
elif "queHint" in question and question.get("queHint") is not None:
|
|
13739
|
+
payload["queHint"] = str(question.get("queHint") or "")
|
|
13740
|
+
if field.get("default_type") is not None:
|
|
13741
|
+
payload["queDefaultType"] = _coerce_positive_int(field.get("default_type")) or 1
|
|
13742
|
+
if "default_value" in field:
|
|
13743
|
+
payload["queDefaultValue"] = field.get("default_value")
|
|
13744
|
+
payload["referenceConfig"] = _normalize_reference_config_for_save(question.get("referenceConfig"), field=field)
|
|
13745
|
+
return payload
|
|
13746
|
+
|
|
13747
|
+
|
|
13748
|
+
def _build_form_save_base_payload(current_schema: dict[str, Any], title: str) -> dict[str, Any]:
|
|
13749
|
+
payload: dict[str, Any] = {"formTitle": title}
|
|
13750
|
+
for key in _FORM_SAVE_BASE_KEYS:
|
|
13751
|
+
if key in current_schema:
|
|
13752
|
+
payload[key] = deepcopy(current_schema.get(key))
|
|
13753
|
+
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
13754
|
+
payload["formQues"] = []
|
|
13755
|
+
payload["questionRelations"] = []
|
|
13756
|
+
return payload
|
|
13757
|
+
|
|
13758
|
+
|
|
13759
|
+
def _normalize_question_relations_for_save(question_relations: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
|
|
13760
|
+
normalized: list[dict[str, Any]] = []
|
|
13761
|
+
for relation in question_relations or []:
|
|
13762
|
+
if not isinstance(relation, dict):
|
|
13763
|
+
continue
|
|
13764
|
+
item: dict[str, Any] = {}
|
|
13765
|
+
for key in _QUESTION_RELATION_SAVE_KEYS:
|
|
13766
|
+
if key not in relation:
|
|
13767
|
+
continue
|
|
13768
|
+
value = relation.get(key)
|
|
13769
|
+
if value is None:
|
|
13770
|
+
continue
|
|
13771
|
+
if key == "matchRuleFormula":
|
|
13772
|
+
value = _encode_formula_for_backend_save(value)
|
|
13773
|
+
item[key] = deepcopy(value)
|
|
13774
|
+
if item:
|
|
13775
|
+
normalized.append(item)
|
|
13776
|
+
return normalized
|
|
13777
|
+
|
|
13778
|
+
|
|
13779
|
+
def _field_rename_maps(fields: list[dict[str, Any]]) -> tuple[dict[int, str], dict[str, str]]:
|
|
13780
|
+
by_que_id: dict[int, str] = {}
|
|
13781
|
+
by_title: dict[str, str] = {}
|
|
13782
|
+
|
|
13783
|
+
def visit(field: dict[str, Any]) -> None:
|
|
13784
|
+
if not isinstance(field, dict):
|
|
13785
|
+
return
|
|
13786
|
+
template = field.get("_question_template")
|
|
13787
|
+
if not isinstance(template, dict):
|
|
13788
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13789
|
+
visit(subfield)
|
|
13790
|
+
return
|
|
13791
|
+
old_title = str(template.get("queTitle") or "").strip()
|
|
13792
|
+
new_title = str(field.get("name") or "").strip()
|
|
13793
|
+
if not old_title or not new_title or old_title == new_title:
|
|
13794
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13795
|
+
visit(subfield)
|
|
13796
|
+
return
|
|
13797
|
+
que_id = _coerce_nonnegative_int(field.get("que_id"))
|
|
13798
|
+
if que_id is None:
|
|
13799
|
+
que_id = _coerce_nonnegative_int(template.get("queId"))
|
|
13800
|
+
if que_id is not None:
|
|
13801
|
+
by_que_id[que_id] = new_title
|
|
13802
|
+
by_title[old_title] = new_title
|
|
13803
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13804
|
+
visit(subfield)
|
|
13805
|
+
|
|
13806
|
+
for field in fields:
|
|
13807
|
+
visit(field)
|
|
13808
|
+
return by_que_id, by_title
|
|
13809
|
+
|
|
13810
|
+
|
|
13811
|
+
def _sync_question_title_references(value: Any, *, by_que_id: dict[int, str], by_title: dict[str, str]) -> None:
|
|
13812
|
+
if isinstance(value, list):
|
|
13813
|
+
for item in value:
|
|
13814
|
+
_sync_question_title_references(item, by_que_id=by_que_id, by_title=by_title)
|
|
13815
|
+
return
|
|
13816
|
+
if not isinstance(value, dict):
|
|
13817
|
+
return
|
|
13818
|
+
|
|
13819
|
+
title_keys = ("queTitle", "_field_id")
|
|
13820
|
+
que_id = _coerce_nonnegative_int(value.get("queId"))
|
|
13821
|
+
replacement = None
|
|
13822
|
+
if que_id is not None and que_id in by_que_id:
|
|
13823
|
+
replacement = by_que_id[que_id]
|
|
13824
|
+
elif que_id is None:
|
|
13825
|
+
for key in title_keys:
|
|
13826
|
+
current_title = str(value.get(key) or "").strip()
|
|
13827
|
+
if current_title and current_title in by_title:
|
|
13828
|
+
replacement = by_title[current_title]
|
|
13829
|
+
break
|
|
13830
|
+
if replacement is not None:
|
|
13831
|
+
for key in title_keys:
|
|
13832
|
+
current_title = str(value.get(key) or "").strip()
|
|
13833
|
+
if (que_id is not None and que_id in by_que_id and key in value) or (current_title and current_title in by_title):
|
|
13834
|
+
value[key] = replacement
|
|
13835
|
+
sup_id = _coerce_nonnegative_int(value.get("supId"))
|
|
13836
|
+
if sup_id is not None and sup_id in by_que_id and "supQueTitle" in value:
|
|
13837
|
+
value["supQueTitle"] = by_que_id[sup_id]
|
|
13838
|
+
|
|
13839
|
+
for child_value in value.values():
|
|
13840
|
+
if isinstance(child_value, (dict, list)):
|
|
13841
|
+
_sync_question_title_references(child_value, by_que_id=by_que_id, by_title=by_title)
|
|
13842
|
+
|
|
13843
|
+
|
|
13844
|
+
def _materialize_preserved_subtable_question(field: dict[str, Any], *, template: dict[str, Any]) -> dict[str, Any] | None:
|
|
13845
|
+
materialized_subquestions: list[dict[str, Any]] = []
|
|
13846
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13847
|
+
if not isinstance(subfield, dict):
|
|
13848
|
+
continue
|
|
13849
|
+
materialized = _materialize_preserved_question(subfield)
|
|
13850
|
+
if materialized is None:
|
|
13851
|
+
return None
|
|
13852
|
+
materialized_subquestions.append(materialized)
|
|
13853
|
+
template["subQuestions"] = materialized_subquestions
|
|
13854
|
+
template["innerQuestions"] = [deepcopy(materialized_subquestions)]
|
|
13855
|
+
return template
|
|
13856
|
+
|
|
13857
|
+
|
|
13858
|
+
def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | None:
|
|
13859
|
+
template = deepcopy(field.get("_question_template"))
|
|
13860
|
+
if not isinstance(template, dict):
|
|
13861
|
+
return None
|
|
13862
|
+
overlay_keys = _field_question_overlay_keys(field)
|
|
13863
|
+
if "name" in overlay_keys:
|
|
13864
|
+
template["queTitle"] = str(field.get("name") or "")
|
|
13865
|
+
if "required" in overlay_keys:
|
|
13866
|
+
template["required"] = bool(field.get("required", False))
|
|
13867
|
+
if "description" in overlay_keys:
|
|
13868
|
+
description = field.get("description")
|
|
13869
|
+
template["queHint"] = "" if description is None else str(description)
|
|
13870
|
+
if str(field.get("type") or "") == FieldType.subtable.value:
|
|
13871
|
+
return _materialize_preserved_subtable_question(field, template=template)
|
|
13872
|
+
if str(field.get("type") or "") == FieldType.relation.value:
|
|
13873
|
+
return _normalize_relation_question_for_save(template, field=field)
|
|
13874
|
+
return template
|
|
13875
|
+
|
|
13876
|
+
|
|
13877
|
+
def _materialize_edit_question(field: dict[str, Any], *, temp_id: int) -> tuple[dict[str, Any], bool]:
|
|
13878
|
+
if not _field_needs_question_rebuild(field):
|
|
13879
|
+
preserved = _materialize_preserved_question(field)
|
|
13880
|
+
if preserved is not None:
|
|
13881
|
+
return preserved, True
|
|
13882
|
+
return _field_to_question(field, temp_id=temp_id), False
|
|
13883
|
+
|
|
13884
|
+
|
|
12693
13885
|
def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]:
|
|
12694
|
-
|
|
13886
|
+
built_question, _next_temp_id = build_question(
|
|
12695
13887
|
{
|
|
12696
13888
|
"label": field["name"],
|
|
12697
13889
|
"type": field["type"],
|
|
@@ -12716,11 +13908,32 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
12716
13908
|
},
|
|
12717
13909
|
temp_id,
|
|
12718
13910
|
)
|
|
13911
|
+
relation_config_explicit = bool(field.get("_relation_config_explicit"))
|
|
13912
|
+
relation_question_template = (
|
|
13913
|
+
deepcopy(field.get("_question_template"))
|
|
13914
|
+
if field.get("type") == FieldType.relation.value and isinstance(field.get("_question_template"), dict)
|
|
13915
|
+
else None
|
|
13916
|
+
)
|
|
13917
|
+
question = (
|
|
13918
|
+
relation_question_template
|
|
13919
|
+
if relation_question_template is not None and not relation_config_explicit
|
|
13920
|
+
else built_question
|
|
13921
|
+
)
|
|
13922
|
+
if relation_config_explicit and relation_question_template is not None:
|
|
13923
|
+
for key in ("queOriginType", "relationDisplayMode", "customRenderConfig"):
|
|
13924
|
+
if key in relation_question_template:
|
|
13925
|
+
question[key] = deepcopy(relation_question_template[key])
|
|
12719
13926
|
if _coerce_nonnegative_int(field.get("que_id")) is not None:
|
|
12720
13927
|
question["queId"] = field["que_id"]
|
|
13928
|
+
question.pop("queTempId", None)
|
|
12721
13929
|
else:
|
|
13930
|
+
question["queId"] = 0
|
|
12722
13931
|
question["queTempId"] = temp_id
|
|
12723
13932
|
field["que_temp_id"] = temp_id
|
|
13933
|
+
question["queType"] = built_question.get("queType", question.get("queType"))
|
|
13934
|
+
question["queTitle"] = built_question.get("queTitle", field["name"])
|
|
13935
|
+
question["required"] = built_question.get("required", bool(field.get("required", False)))
|
|
13936
|
+
question["queHint"] = built_question.get("queHint", field.get("description") or "")
|
|
12724
13937
|
if field.get("default_type") is not None:
|
|
12725
13938
|
question["queDefaultType"] = _coerce_positive_int(field.get("default_type")) or 1
|
|
12726
13939
|
if "default_value" in field:
|
|
@@ -12728,21 +13941,60 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
12728
13941
|
if field.get("type") == FieldType.relation.value:
|
|
12729
13942
|
preserved_reference = (
|
|
12730
13943
|
deepcopy(field.get("_reference_config_template"))
|
|
12731
|
-
if not
|
|
13944
|
+
if not relation_config_explicit and isinstance(field.get("_reference_config_template"), dict)
|
|
12732
13945
|
else None
|
|
12733
13946
|
)
|
|
12734
13947
|
if preserved_reference is not None:
|
|
12735
13948
|
preserved_reference["referAppKey"] = field.get("target_app_key")
|
|
12736
|
-
preserved_reference["_targetEntityId"] = field.get("target_app_key")
|
|
12737
13949
|
question["referenceConfig"] = preserved_reference
|
|
12738
13950
|
else:
|
|
12739
|
-
|
|
13951
|
+
existing_reference = (
|
|
13952
|
+
deepcopy(relation_question_template.get("referenceConfig"))
|
|
13953
|
+
if relation_question_template is not None and isinstance(relation_question_template.get("referenceConfig"), dict)
|
|
13954
|
+
else deepcopy(question.get("referenceConfig"))
|
|
13955
|
+
if isinstance(question.get("referenceConfig"), dict)
|
|
13956
|
+
else {}
|
|
13957
|
+
)
|
|
13958
|
+
reference = (
|
|
13959
|
+
existing_reference
|
|
13960
|
+
if relation_config_explicit
|
|
13961
|
+
else deepcopy(question.get("referenceConfig"))
|
|
13962
|
+
if isinstance(question.get("referenceConfig"), dict)
|
|
13963
|
+
else {}
|
|
13964
|
+
)
|
|
13965
|
+
built_reference = (
|
|
13966
|
+
deepcopy(built_question.get("referenceConfig"))
|
|
13967
|
+
if isinstance(built_question.get("referenceConfig"), dict)
|
|
13968
|
+
else {}
|
|
13969
|
+
)
|
|
13970
|
+
original_target_app_key = str(existing_reference.get("referAppKey") or "").strip()
|
|
13971
|
+
next_target_app_key = str(field.get("target_app_key") or "").strip()
|
|
13972
|
+
preserve_existing_reference_questions = (
|
|
13973
|
+
relation_config_explicit
|
|
13974
|
+
and bool(original_target_app_key)
|
|
13975
|
+
and original_target_app_key == next_target_app_key
|
|
13976
|
+
)
|
|
13977
|
+
if relation_config_explicit:
|
|
13978
|
+
for stale_key in ("customButtonText", "customAdvancedSetting", "configShowForm", "dataShowForm"):
|
|
13979
|
+
reference.pop(stale_key, None)
|
|
13980
|
+
for key in (
|
|
13981
|
+
"referQueId",
|
|
13982
|
+
"referQuestions",
|
|
13983
|
+
"referAuthQues",
|
|
13984
|
+
"optionalDataNum",
|
|
13985
|
+
"fieldNameShow",
|
|
13986
|
+
"_targetFieldId",
|
|
13987
|
+
):
|
|
13988
|
+
if preserve_existing_reference_questions and key in {"referQuestions", "referAuthQues"}:
|
|
13989
|
+
continue
|
|
13990
|
+
if key in built_reference:
|
|
13991
|
+
reference[key] = deepcopy(built_reference[key])
|
|
12740
13992
|
reference["referAppKey"] = field.get("target_app_key")
|
|
12741
13993
|
reference["_targetEntityId"] = field.get("target_app_key")
|
|
12742
13994
|
if field.get("target_field_que_id") is not None:
|
|
12743
13995
|
reference["referQueId"] = field.get("target_field_que_id")
|
|
12744
|
-
reference["optionalDataNum"] = _relation_mode_to_optional_data_num(field.get("relation_mode"))
|
|
12745
13996
|
question["referenceConfig"] = reference
|
|
13997
|
+
question = _normalize_relation_question_for_save(question, field=field)
|
|
12746
13998
|
if field.get("type") == FieldType.department.value:
|
|
12747
13999
|
scope_type, scope_payload = _serialize_department_scope_for_question(field.get("department_scope"))
|
|
12748
14000
|
question["deptSelectScopeType"] = scope_type
|
|
@@ -12882,8 +14134,127 @@ def _build_form_payload_from_fields(
|
|
|
12882
14134
|
for row in form_rows:
|
|
12883
14135
|
_apply_row_widths(row)
|
|
12884
14136
|
payload = default_form_payload(title, form_rows)
|
|
14137
|
+
_normalize_formula_defaults_for_save(payload.get("formQues"))
|
|
14138
|
+
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
14139
|
+
payload["questionRelations"] = _normalize_question_relations_for_save(
|
|
14140
|
+
question_relations if question_relations is not None else (current_schema.get("questionRelations") or [])
|
|
14141
|
+
)
|
|
14142
|
+
return payload
|
|
14143
|
+
|
|
14144
|
+
|
|
14145
|
+
def _build_form_payload_for_edit_fields(
|
|
14146
|
+
*,
|
|
14147
|
+
title: str,
|
|
14148
|
+
current_schema: dict[str, Any],
|
|
14149
|
+
fields: list[dict[str, Any]],
|
|
14150
|
+
layout: dict[str, Any],
|
|
14151
|
+
question_relations: list[dict[str, Any]] | None = None,
|
|
14152
|
+
) -> dict[str, Any]:
|
|
14153
|
+
_, section_templates = _extract_question_templates(current_schema)
|
|
14154
|
+
template_row_lengths_by_que_id, template_row_lengths_by_title = _extract_template_row_lengths(current_schema)
|
|
14155
|
+
fields_by_name = {
|
|
14156
|
+
str(field.get("name") or ""): field
|
|
14157
|
+
for field in fields
|
|
14158
|
+
if isinstance(field, dict) and str(field.get("name") or "").strip()
|
|
14159
|
+
}
|
|
14160
|
+
form_rows: list[list[dict[str, Any]]] = []
|
|
14161
|
+
temp_id = -10000
|
|
14162
|
+
|
|
14163
|
+
for row in layout.get("root_rows", []) or []:
|
|
14164
|
+
questions: list[dict[str, Any]] = []
|
|
14165
|
+
expected_template_lengths: list[int] = []
|
|
14166
|
+
row_preserved = True
|
|
14167
|
+
for name in row:
|
|
14168
|
+
field = fields_by_name.get(str(name))
|
|
14169
|
+
if field is None:
|
|
14170
|
+
continue
|
|
14171
|
+
template_row_length = _field_template_row_length(
|
|
14172
|
+
field,
|
|
14173
|
+
lengths_by_que_id=template_row_lengths_by_que_id,
|
|
14174
|
+
lengths_by_title=template_row_lengths_by_title,
|
|
14175
|
+
)
|
|
14176
|
+
if template_row_length is not None:
|
|
14177
|
+
expected_template_lengths.append(template_row_length)
|
|
14178
|
+
question, preserved = _materialize_edit_question(field, temp_id=temp_id)
|
|
14179
|
+
questions.append(question)
|
|
14180
|
+
row_preserved = row_preserved and preserved
|
|
14181
|
+
temp_id -= 100
|
|
14182
|
+
if not questions:
|
|
14183
|
+
continue
|
|
14184
|
+
if not row_preserved or _row_needs_width_reflow(expected_template_lengths, len(questions)):
|
|
14185
|
+
_apply_row_widths(questions)
|
|
14186
|
+
form_rows.append(questions)
|
|
14187
|
+
|
|
14188
|
+
for section in layout.get("sections", []) or []:
|
|
14189
|
+
inner_rows: list[list[dict[str, Any]]] = []
|
|
14190
|
+
for row in section.get("rows", []) or []:
|
|
14191
|
+
questions: list[dict[str, Any]] = []
|
|
14192
|
+
expected_template_lengths: list[int] = []
|
|
14193
|
+
row_preserved = True
|
|
14194
|
+
for name in row:
|
|
14195
|
+
field = fields_by_name.get(str(name))
|
|
14196
|
+
if field is None:
|
|
14197
|
+
continue
|
|
14198
|
+
template_row_length = _field_template_row_length(
|
|
14199
|
+
field,
|
|
14200
|
+
lengths_by_que_id=template_row_lengths_by_que_id,
|
|
14201
|
+
lengths_by_title=template_row_lengths_by_title,
|
|
14202
|
+
)
|
|
14203
|
+
if template_row_length is not None:
|
|
14204
|
+
expected_template_lengths.append(template_row_length)
|
|
14205
|
+
question, preserved = _materialize_edit_question(field, temp_id=temp_id)
|
|
14206
|
+
questions.append(question)
|
|
14207
|
+
row_preserved = row_preserved and preserved
|
|
14208
|
+
temp_id -= 100
|
|
14209
|
+
if not questions:
|
|
14210
|
+
continue
|
|
14211
|
+
if not row_preserved or _row_needs_width_reflow(expected_template_lengths, len(questions)):
|
|
14212
|
+
_apply_row_widths(questions)
|
|
14213
|
+
inner_rows.append(questions)
|
|
14214
|
+
if not inner_rows:
|
|
14215
|
+
continue
|
|
14216
|
+
template = _select_section_template(section_templates, section)
|
|
14217
|
+
wrapper = deepcopy(template) if isinstance(template, dict) else {
|
|
14218
|
+
"queId": 0,
|
|
14219
|
+
"queTempId": -(20000 + sum(ord(ch) for ch in str(section.get("section_id") or section.get("title") or "section"))),
|
|
14220
|
+
"queType": 24,
|
|
14221
|
+
"queWidth": 100,
|
|
14222
|
+
"scanType": 1,
|
|
14223
|
+
"status": 1,
|
|
14224
|
+
"required": False,
|
|
14225
|
+
"queHint": "",
|
|
14226
|
+
"linkedQuestions": {},
|
|
14227
|
+
"logicalShow": True,
|
|
14228
|
+
"queDefaultValue": None,
|
|
14229
|
+
"queDefaultType": 1,
|
|
14230
|
+
"subQueWidth": 2,
|
|
14231
|
+
"beingHide": False,
|
|
14232
|
+
"beingDesensitized": False,
|
|
14233
|
+
}
|
|
14234
|
+
if section.get("title") is not None:
|
|
14235
|
+
wrapper["queTitle"] = section.get("title") or wrapper.get("queTitle") or "未命名分组"
|
|
14236
|
+
parsed_section_id = _coerce_positive_int(section.get("section_id"))
|
|
14237
|
+
if parsed_section_id is not None:
|
|
14238
|
+
wrapper["sectionId"] = parsed_section_id
|
|
14239
|
+
elif template is None and section.get("section_id") is not None:
|
|
14240
|
+
wrapper["sectionId"] = section.get("section_id")
|
|
14241
|
+
wrapper["innerQuestions"] = inner_rows
|
|
14242
|
+
form_rows.append([wrapper])
|
|
14243
|
+
|
|
14244
|
+
rename_by_que_id, rename_by_title = _field_rename_maps(fields)
|
|
14245
|
+
if rename_by_que_id or rename_by_title:
|
|
14246
|
+
_sync_question_title_references(form_rows, by_que_id=rename_by_que_id, by_title=rename_by_title)
|
|
14247
|
+
normalized_relations = _normalize_question_relations_for_save(
|
|
14248
|
+
question_relations if question_relations is not None else (current_schema.get("questionRelations") or [])
|
|
14249
|
+
)
|
|
14250
|
+
if rename_by_que_id or rename_by_title:
|
|
14251
|
+
_sync_question_title_references(normalized_relations, by_que_id=rename_by_que_id, by_title=rename_by_title)
|
|
14252
|
+
|
|
14253
|
+
payload = _build_form_save_base_payload(current_schema, title)
|
|
14254
|
+
payload["formQues"] = form_rows
|
|
14255
|
+
_normalize_formula_defaults_for_save(payload.get("formQues"))
|
|
14256
|
+
payload["questionRelations"] = normalized_relations
|
|
12885
14257
|
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
12886
|
-
payload["questionRelations"] = deepcopy(question_relations if question_relations is not None else (current_schema.get("questionRelations") or []))
|
|
12887
14258
|
return payload
|
|
12888
14259
|
|
|
12889
14260
|
|
|
@@ -13048,6 +14419,8 @@ def _summarize_views(result: Any) -> list[dict[str, Any]]:
|
|
|
13048
14419
|
view_type = _normalize_view_type_name(view.get("viewgraphType") or view.get("type"))
|
|
13049
14420
|
columns = view.get("columnNames") or view.get("columns") or []
|
|
13050
14421
|
group_by = view.get("groupBy") or view.get("group_by")
|
|
14422
|
+
if not any((name, view_type, columns, group_by)) and str(view_key or "").isdigit():
|
|
14423
|
+
continue
|
|
13051
14424
|
if not any((name, view_key, view_type, columns, group_by)):
|
|
13052
14425
|
continue
|
|
13053
14426
|
items.append(
|
|
@@ -13090,11 +14463,24 @@ def _summarize_views_with_config(views_tool: ViewTools, *, profile: str, views:
|
|
|
13090
14463
|
enriched_items.append(item)
|
|
13091
14464
|
continue
|
|
13092
14465
|
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
13093
|
-
|
|
14466
|
+
question_list: list[dict[str, Any]] = []
|
|
14467
|
+
try:
|
|
14468
|
+
question_response = views_tool.view_list_questions(profile=profile, viewgraph_key=view_key)
|
|
14469
|
+
raw_question_list = question_response.get("result")
|
|
14470
|
+
if isinstance(raw_question_list, list):
|
|
14471
|
+
question_list = [deepcopy(entry) for entry in raw_question_list if isinstance(entry, dict)]
|
|
14472
|
+
except (QingflowApiError, RuntimeError):
|
|
14473
|
+
question_list = []
|
|
14474
|
+
enriched_items.append(_merge_view_summary_with_config(item, config=config, question_list=question_list))
|
|
13094
14475
|
return enriched_items, config_read_errors
|
|
13095
14476
|
|
|
13096
14477
|
|
|
13097
|
-
def _merge_view_summary_with_config(
|
|
14478
|
+
def _merge_view_summary_with_config(
|
|
14479
|
+
base: dict[str, Any],
|
|
14480
|
+
*,
|
|
14481
|
+
config: dict[str, Any],
|
|
14482
|
+
question_list: list[dict[str, Any]] | None = None,
|
|
14483
|
+
) -> dict[str, Any]:
|
|
13098
14484
|
summary = deepcopy(base)
|
|
13099
14485
|
if not isinstance(config, dict) or not config:
|
|
13100
14486
|
return summary
|
|
@@ -13102,6 +14488,7 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
13102
14488
|
summary["visibility_summary"] = _visibility_summary(_public_visibility_from_member_auth(config.get("auth")))
|
|
13103
14489
|
legacy_columns = [str(value) for value in (summary.get("columns") or []) if str(value or "").strip()]
|
|
13104
14490
|
question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
|
|
14491
|
+
canonical_question_entries = _extract_view_question_entries(question_list)
|
|
13105
14492
|
question_entries_by_id = {
|
|
13106
14493
|
field_id: entry
|
|
13107
14494
|
for entry in question_entries
|
|
@@ -13121,19 +14508,20 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
13121
14508
|
display_entries = _sort_view_question_entries(
|
|
13122
14509
|
[entry for entry in question_entries if bool(entry.get("visible", True))],
|
|
13123
14510
|
)
|
|
14511
|
+
public_display_entries = _filter_public_view_display_entries(display_entries, configured_column_ids=configured_column_ids)
|
|
13124
14512
|
display_column_ids = [
|
|
13125
14513
|
field_id
|
|
13126
|
-
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in
|
|
14514
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in public_display_entries)
|
|
13127
14515
|
if field_id is not None
|
|
13128
14516
|
]
|
|
13129
14517
|
display_columns = [
|
|
13130
14518
|
str(entry.get("name") or "").strip()
|
|
13131
|
-
for entry in
|
|
14519
|
+
for entry in public_display_entries
|
|
13132
14520
|
if str(entry.get("name") or "").strip()
|
|
13133
14521
|
]
|
|
13134
14522
|
apply_entries = [
|
|
13135
14523
|
entry
|
|
13136
|
-
for entry in
|
|
14524
|
+
for entry in public_display_entries
|
|
13137
14525
|
if _coerce_nonnegative_int(entry.get("field_id")) is not None
|
|
13138
14526
|
and str(entry.get("name") or "").strip()
|
|
13139
14527
|
and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
@@ -13180,8 +14568,42 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
13180
14568
|
summary["apply_columns"] = apply_columns
|
|
13181
14569
|
summary["apply_column_ids"] = apply_column_ids
|
|
13182
14570
|
config_enriched = True
|
|
13183
|
-
if
|
|
13184
|
-
|
|
14571
|
+
if canonical_question_entries:
|
|
14572
|
+
canonical_display_entries = _sort_view_question_entries(canonical_question_entries)
|
|
14573
|
+
canonical_display_column_ids = [
|
|
14574
|
+
field_id
|
|
14575
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in canonical_display_entries)
|
|
14576
|
+
if field_id is not None
|
|
14577
|
+
]
|
|
14578
|
+
canonical_display_columns = [
|
|
14579
|
+
str(entry.get("name") or "").strip()
|
|
14580
|
+
for entry in canonical_display_entries
|
|
14581
|
+
if str(entry.get("name") or "").strip()
|
|
14582
|
+
]
|
|
14583
|
+
canonical_apply_entries = [
|
|
14584
|
+
entry
|
|
14585
|
+
for entry in canonical_display_entries
|
|
14586
|
+
if _coerce_nonnegative_int(entry.get("field_id")) is not None
|
|
14587
|
+
and str(entry.get("name") or "").strip()
|
|
14588
|
+
and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
14589
|
+
]
|
|
14590
|
+
summary["columns"] = canonical_display_columns
|
|
14591
|
+
summary["display_columns"] = canonical_display_columns
|
|
14592
|
+
summary["display_column_ids"] = canonical_display_column_ids
|
|
14593
|
+
summary["column_details"] = canonical_display_entries
|
|
14594
|
+
summary["apply_columns"] = [
|
|
14595
|
+
str(entry.get("name") or "").strip()
|
|
14596
|
+
for entry in canonical_apply_entries
|
|
14597
|
+
if str(entry.get("name") or "").strip()
|
|
14598
|
+
]
|
|
14599
|
+
summary["apply_column_ids"] = [
|
|
14600
|
+
field_id
|
|
14601
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in canonical_apply_entries)
|
|
14602
|
+
if field_id is not None
|
|
14603
|
+
]
|
|
14604
|
+
config_enriched = True
|
|
14605
|
+
elif question_entries:
|
|
14606
|
+
summary["column_details"] = public_display_entries or _sort_view_question_entries(question_entries)
|
|
13185
14607
|
config_enriched = True
|
|
13186
14608
|
display_config = _extract_view_display_config(
|
|
13187
14609
|
config,
|
|
@@ -13206,7 +14628,7 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
13206
14628
|
summary["button_read_source"] = button_source
|
|
13207
14629
|
config_enriched = True
|
|
13208
14630
|
if config_enriched:
|
|
13209
|
-
summary["read_source"] = "view_config"
|
|
14631
|
+
summary["read_source"] = "view_config+question" if canonical_question_entries else "view_config"
|
|
13210
14632
|
return summary
|
|
13211
14633
|
|
|
13212
14634
|
|
|
@@ -13214,29 +14636,64 @@ def _extract_view_question_entries(questions: Any) -> list[dict[str, Any]]:
|
|
|
13214
14636
|
if not isinstance(questions, list):
|
|
13215
14637
|
return []
|
|
13216
14638
|
entries: list[dict[str, Any]] = []
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
13222
|
-
|
|
13223
|
-
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
14639
|
+
fallback_order = 0
|
|
14640
|
+
|
|
14641
|
+
def walk(nodes: Any) -> None:
|
|
14642
|
+
nonlocal fallback_order
|
|
14643
|
+
if not isinstance(nodes, list):
|
|
14644
|
+
return
|
|
14645
|
+
for item in nodes:
|
|
14646
|
+
if not isinstance(item, dict):
|
|
14647
|
+
continue
|
|
14648
|
+
children: list[Any] = []
|
|
14649
|
+
for child_key in ("innerQues", "subQues", "innerQuestions", "subQuestions"):
|
|
14650
|
+
child_value = item.get(child_key)
|
|
14651
|
+
if isinstance(child_value, list) and child_value:
|
|
14652
|
+
children.extend(child_value)
|
|
14653
|
+
if children:
|
|
14654
|
+
walk(children)
|
|
14655
|
+
continue
|
|
14656
|
+
field_id = _coerce_nonnegative_int(item.get("queId"))
|
|
14657
|
+
name = str(item.get("queTitle") or "").strip() or None
|
|
14658
|
+
if field_id is None and name is None:
|
|
14659
|
+
continue
|
|
14660
|
+
visible_raw = item.get("beingListDisplay")
|
|
14661
|
+
if visible_raw is None:
|
|
14662
|
+
visible_raw = item.get("beingVisible")
|
|
14663
|
+
visible = bool(visible_raw) if visible_raw is not None else True
|
|
14664
|
+
display_order = _coerce_positive_int(item.get("displayOrdinal"))
|
|
14665
|
+
fallback_order += 1
|
|
14666
|
+
entry: dict[str, Any] = {
|
|
14667
|
+
"field_id": field_id,
|
|
14668
|
+
"name": name,
|
|
14669
|
+
"visible": visible,
|
|
14670
|
+
"display_order": display_order if display_order is not None else fallback_order,
|
|
14671
|
+
}
|
|
14672
|
+
width = _coerce_positive_int(item.get("width"))
|
|
14673
|
+
if width is not None:
|
|
14674
|
+
entry["width"] = width
|
|
14675
|
+
entries.append(entry)
|
|
14676
|
+
|
|
14677
|
+
walk(questions)
|
|
13237
14678
|
return entries
|
|
13238
14679
|
|
|
13239
14680
|
|
|
14681
|
+
def _filter_public_view_display_entries(
|
|
14682
|
+
entries: list[dict[str, Any]],
|
|
14683
|
+
*,
|
|
14684
|
+
configured_column_ids: list[int],
|
|
14685
|
+
) -> list[dict[str, Any]]:
|
|
14686
|
+
configured_set = set(configured_column_ids)
|
|
14687
|
+
filtered: list[dict[str, Any]] = []
|
|
14688
|
+
for entry in entries:
|
|
14689
|
+
name = str(entry.get("name") or "").strip()
|
|
14690
|
+
field_id = _coerce_nonnegative_int(entry.get("field_id"))
|
|
14691
|
+
if name in _KNOWN_SYSTEM_VIEW_COLUMNS and field_id not in configured_set:
|
|
14692
|
+
continue
|
|
14693
|
+
filtered.append(entry)
|
|
14694
|
+
return filtered or entries
|
|
14695
|
+
|
|
14696
|
+
|
|
13240
14697
|
def _sort_view_question_entries(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
13241
14698
|
return sorted(
|
|
13242
14699
|
entries,
|
|
@@ -14296,9 +15753,10 @@ def _build_form_payload_from_existing_schema(
|
|
|
14296
15753
|
wrapper["queWidth"] = 100
|
|
14297
15754
|
form_rows.append([wrapper])
|
|
14298
15755
|
|
|
14299
|
-
payload =
|
|
15756
|
+
payload = _build_form_save_base_payload(current_schema, str(current_schema.get("formTitle") or "未命名应用"))
|
|
14300
15757
|
payload["formQues"] = form_rows
|
|
14301
|
-
payload
|
|
15758
|
+
_normalize_formula_defaults_for_save(payload.get("formQues"))
|
|
15759
|
+
payload["questionRelations"] = _normalize_question_relations_for_save(current_schema.get("questionRelations") or [])
|
|
14302
15760
|
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
14303
15761
|
payload.setdefault("formTitle", current_schema.get("formTitle") or "未命名应用")
|
|
14304
15762
|
return payload
|