@qingflow-tech/qingflow-app-user-mcp 1.0.2 → 1.0.4
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-user/SKILL.md +21 -12
- package/skills/qingflow-app-user/references/data-gotchas.md +1 -1
- package/skills/qingflow-app-user/references/public-surface-sync.md +70 -0
- package/skills/qingflow-app-user/references/record-patterns.md +1 -1
- package/skills/qingflow-record-analysis/SKILL.md +44 -2
- package/skills/qingflow-record-insert/SKILL.md +3 -0
- package/skills/qingflow-record-update/SKILL.md +3 -0
- package/skills/qingflow-task-ops/SKILL.md +31 -10
- package/src/qingflow_mcp/__init__.py +33 -1
- package/src/qingflow_mcp/backend_client.py +109 -0
- package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
- package/src/qingflow_mcp/builder_facade/models.py +58 -9
- package/src/qingflow_mcp/builder_facade/service.py +1711 -240
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/app.py +47 -1
- package/src/qingflow_mcp/cli/commands/auth.py +63 -0
- package/src/qingflow_mcp/cli/commands/builder.py +11 -3
- package/src/qingflow_mcp/cli/commands/exports.py +111 -0
- package/src/qingflow_mcp/cli/commands/record.py +5 -5
- package/src/qingflow_mcp/cli/commands/task.py +701 -27
- package/src/qingflow_mcp/cli/commands/workspace.py +84 -0
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +424 -50
- package/src/qingflow_mcp/cli/interaction.py +72 -0
- package/src/qingflow_mcp/cli/main.py +11 -1
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
- package/src/qingflow_mcp/config.py +1 -1
- package/src/qingflow_mcp/errors.py +4 -4
- package/src/qingflow_mcp/export_store.py +14 -0
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/public_surface.py +16 -1
- package/src/qingflow_mcp/response_trim.py +394 -9
- package/src/qingflow_mcp/server.py +26 -0
- package/src/qingflow_mcp/server_app_builder.py +15 -1
- package/src/qingflow_mcp/server_app_user.py +113 -0
- package/src/qingflow_mcp/session_store.py +126 -21
- 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 +107 -34
- package/src/qingflow_mcp/tools/app_tools.py +1 -0
- package/src/qingflow_mcp/tools/auth_tools.py +243 -9
- 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/custom_button_tools.py +0 -2
- package/src/qingflow_mcp/tools/export_tools.py +1565 -0
- package/src/qingflow_mcp/tools/import_tools.py +78 -4
- package/src/qingflow_mcp/tools/record_tools.py +551 -165
- package/src/qingflow_mcp/tools/resource_read_tools.py +154 -33
- package/src/qingflow_mcp/tools/task_context_tools.py +917 -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
|
|
@@ -30,6 +34,7 @@ from ..tools.role_tools import RoleTools
|
|
|
30
34
|
from ..tools.solution_tools import SolutionTools
|
|
31
35
|
from ..tools.view_tools import ViewTools
|
|
32
36
|
from ..tools.workflow_tools import WorkflowTools
|
|
37
|
+
from .button_style_catalog import button_style_catalog_payload
|
|
33
38
|
from .models import (
|
|
34
39
|
AppChartsReadResponse,
|
|
35
40
|
AppFieldsReadResponse,
|
|
@@ -143,6 +148,7 @@ JUDGE_EQUAL_ANY = 9
|
|
|
143
148
|
JUDGE_FUZZY_MATCH = 19
|
|
144
149
|
JUDGE_INCLUDE_ANY = 20
|
|
145
150
|
DEFAULT_TYPE_RELATION = 2
|
|
151
|
+
DEFAULT_TYPE_FORMULA = 3
|
|
146
152
|
RELATION_TYPE_Q_LINKER = 2
|
|
147
153
|
RELATION_TYPE_CODE_BLOCK = 3
|
|
148
154
|
|
|
@@ -440,6 +446,7 @@ class AiBuilderFacade:
|
|
|
440
446
|
base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
|
|
441
447
|
summary = detail_result.get("summary") if isinstance(detail_result, dict) and isinstance(detail_result.get("summary"), dict) else {}
|
|
442
448
|
source = detail if detail else base
|
|
449
|
+
layout_tag_items = _select_package_layout_tag_items(detail=detail, base=base)
|
|
443
450
|
warnings: list[JSONObject] = []
|
|
444
451
|
if detail_read_error is not None:
|
|
445
452
|
warnings.append(
|
|
@@ -450,7 +457,7 @@ class AiBuilderFacade:
|
|
|
450
457
|
"http_status": detail_read_error.http_status,
|
|
451
458
|
}
|
|
452
459
|
)
|
|
453
|
-
public_items = _public_package_items_from_tag_items(
|
|
460
|
+
public_items = _public_package_items_from_tag_items(layout_tag_items)
|
|
454
461
|
item_count = summary.get("itemCount")
|
|
455
462
|
if not isinstance(item_count, int) or item_count < 0 or (item_count == 0 and public_items):
|
|
456
463
|
item_count = len(public_items)
|
|
@@ -508,6 +515,8 @@ class AiBuilderFacade:
|
|
|
508
515
|
}
|
|
509
516
|
effective_package_id = _coerce_positive_int(package_id)
|
|
510
517
|
created = False
|
|
518
|
+
create_result: JSONObject | None = None
|
|
519
|
+
update_result: JSONObject | None = None
|
|
511
520
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
512
521
|
|
|
513
522
|
if effective_package_id is None:
|
|
@@ -598,11 +607,36 @@ class AiBuilderFacade:
|
|
|
598
607
|
)
|
|
599
608
|
except VisibilityResolutionError:
|
|
600
609
|
expected_visibility = None
|
|
610
|
+
metadata_verified = True
|
|
611
|
+
if metadata_requested and update_result is not None:
|
|
612
|
+
metadata_verified = bool(update_result.get("verified"))
|
|
613
|
+
elif created and create_result is not None:
|
|
614
|
+
metadata_verified = bool(create_result.get("verified"))
|
|
615
|
+
layout_verified = True
|
|
616
|
+
if items is not None and layout_result is not None:
|
|
617
|
+
layout_verified = bool(layout_result.get("verified"))
|
|
618
|
+
response_verification: JSONObject = {
|
|
619
|
+
"package_exists": True,
|
|
620
|
+
"package_created": created,
|
|
621
|
+
"layout_applied": items is not None,
|
|
622
|
+
"metadata_verified": metadata_verified,
|
|
623
|
+
"layout_verified": layout_verified,
|
|
624
|
+
"visibility_verified": None
|
|
625
|
+
if expected_visibility is None
|
|
626
|
+
else _visibility_matches_expected(verification.get("visibility"), expected_visibility),
|
|
627
|
+
}
|
|
628
|
+
if isinstance(update_result, dict):
|
|
629
|
+
update_verification = update_result.get("verification")
|
|
630
|
+
if isinstance(update_verification, dict):
|
|
631
|
+
for key in ("package_name_verified", "package_icon_verified", "visibility_verified"):
|
|
632
|
+
if key in update_verification:
|
|
633
|
+
response_verification[key] = deepcopy(update_verification.get(key))
|
|
634
|
+
response_verified = metadata_verified and layout_verified and response_verification.get("visibility_verified") is not False
|
|
601
635
|
response: JSONObject = {
|
|
602
|
-
"status": "success",
|
|
636
|
+
"status": "success" if response_verified else "partial_success",
|
|
603
637
|
"error_code": None,
|
|
604
638
|
"recoverable": False,
|
|
605
|
-
"message": "applied package",
|
|
639
|
+
"message": "applied package" if response_verified else "applied package with unverified readback",
|
|
606
640
|
"normalized_args": normalized_args,
|
|
607
641
|
"missing_fields": [],
|
|
608
642
|
"allowed_values": {},
|
|
@@ -611,15 +645,8 @@ class AiBuilderFacade:
|
|
|
611
645
|
"suggested_next_call": None,
|
|
612
646
|
"noop": not (created or metadata_requested or items is not None),
|
|
613
647
|
"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,
|
|
648
|
+
"verification": response_verification,
|
|
649
|
+
"verified": response_verified,
|
|
623
650
|
**{
|
|
624
651
|
key: deepcopy(value)
|
|
625
652
|
for key, value in verification.items()
|
|
@@ -677,7 +704,7 @@ class AiBuilderFacade:
|
|
|
677
704
|
)
|
|
678
705
|
raw_current = current.get("result") if isinstance(current.get("result"), dict) else {}
|
|
679
706
|
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
|
|
707
|
+
current_name = str(raw_current.get("tagName") or raw_current_base.get("tagName") or "").strip() or None
|
|
681
708
|
desired_name = str(package_name or current_name or "").strip() or current_name or "未命名应用包"
|
|
682
709
|
desired_icon = encode_workspace_icon_with_defaults(
|
|
683
710
|
icon=icon,
|
|
@@ -718,27 +745,33 @@ class AiBuilderFacade:
|
|
|
718
745
|
verification = self.package_get(profile=profile, package_id=tag_id)
|
|
719
746
|
if verification.get("status") != "success":
|
|
720
747
|
return verification
|
|
748
|
+
package_name_verified = str(verification.get("package_name") or "").strip() == desired_name
|
|
749
|
+
package_icon_verified = str(verification.get("icon") or "").strip() == desired_icon
|
|
750
|
+
visibility_verified = _visibility_matches_expected(
|
|
751
|
+
verification.get("visibility"),
|
|
752
|
+
_public_visibility_from_member_auth(desired_auth),
|
|
753
|
+
)
|
|
754
|
+
verified = package_name_verified and package_icon_verified and visibility_verified
|
|
721
755
|
return {
|
|
722
|
-
"status": "success",
|
|
756
|
+
"status": "success" if verified else "partial_success",
|
|
723
757
|
"error_code": None,
|
|
724
758
|
"recoverable": False,
|
|
725
|
-
"message": "updated package",
|
|
759
|
+
"message": "updated package" if verified else "updated package with unverified readback",
|
|
726
760
|
"normalized_args": normalized_args,
|
|
727
761
|
"missing_fields": [],
|
|
728
762
|
"allowed_values": {},
|
|
729
763
|
"details": {},
|
|
730
764
|
"request_id": update_result.get("request_id") if isinstance(update_result, dict) else None,
|
|
731
|
-
"suggested_next_call": None,
|
|
765
|
+
"suggested_next_call": None if verified else {"tool_name": "package_get", "arguments": {"profile": profile, "package_id": tag_id}},
|
|
732
766
|
"noop": False,
|
|
733
767
|
"warnings": [],
|
|
734
768
|
"verification": {
|
|
735
769
|
"package_exists": True,
|
|
736
|
-
"
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
),
|
|
770
|
+
"package_name_verified": package_name_verified,
|
|
771
|
+
"package_icon_verified": package_icon_verified,
|
|
772
|
+
"visibility_verified": visibility_verified,
|
|
740
773
|
},
|
|
741
|
-
"verified":
|
|
774
|
+
"verified": verified,
|
|
742
775
|
**{
|
|
743
776
|
key: deepcopy(value)
|
|
744
777
|
for key, value in verification.items()
|
|
@@ -881,9 +914,7 @@ class AiBuilderFacade:
|
|
|
881
914
|
if isinstance(current_base_result, dict) and isinstance(current_base_result.get("result"), dict)
|
|
882
915
|
else {}
|
|
883
916
|
)
|
|
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
|
|
917
|
+
raw_tag_items = _select_package_layout_tag_items(detail=detail_raw, base=base_raw)
|
|
887
918
|
if not isinstance(raw_tag_items, list):
|
|
888
919
|
return _failed(
|
|
889
920
|
"PACKAGE_LAYOUT_UNREADABLE",
|
|
@@ -1409,6 +1440,8 @@ class AiBuilderFacade:
|
|
|
1409
1440
|
issues: list[dict[str, Any]] = []
|
|
1410
1441
|
resolved: list[dict[str, Any]] = []
|
|
1411
1442
|
seen_ids: set[int] = set()
|
|
1443
|
+
if not dept_ids and not dept_names:
|
|
1444
|
+
return {"department_entries": resolved, "issues": issues}
|
|
1412
1445
|
listed = self.directory.directory_list_all_departments(
|
|
1413
1446
|
profile=profile,
|
|
1414
1447
|
parent_dept_id=None,
|
|
@@ -2321,6 +2354,26 @@ class AiBuilderFacade:
|
|
|
2321
2354
|
**match,
|
|
2322
2355
|
}
|
|
2323
2356
|
|
|
2357
|
+
def button_style_catalog_get(self, *, profile: str) -> JSONObject:
|
|
2358
|
+
return {
|
|
2359
|
+
"status": "success",
|
|
2360
|
+
"error_code": None,
|
|
2361
|
+
"recoverable": False,
|
|
2362
|
+
"message": "read button style catalog",
|
|
2363
|
+
"normalized_args": {},
|
|
2364
|
+
"missing_fields": [],
|
|
2365
|
+
"allowed_values": {},
|
|
2366
|
+
"details": {},
|
|
2367
|
+
"request_id": None,
|
|
2368
|
+
"suggested_next_call": None,
|
|
2369
|
+
"noop": False,
|
|
2370
|
+
"warnings": [],
|
|
2371
|
+
"verification": {"button_style_catalog_verified": True},
|
|
2372
|
+
"verified": True,
|
|
2373
|
+
"profile": profile,
|
|
2374
|
+
**button_style_catalog_payload(),
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2324
2377
|
def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
|
|
2325
2378
|
normalized_args = {"app_key": app_key}
|
|
2326
2379
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
@@ -2780,6 +2833,19 @@ class AiBuilderFacade:
|
|
|
2780
2833
|
"can_copy_app": _coerce_optional_bool(base.get("copyAppStatus")),
|
|
2781
2834
|
}
|
|
2782
2835
|
|
|
2836
|
+
def _derive_can_edit_app_base(self, *, profile: str, permission_summary: JSONObject) -> bool:
|
|
2837
|
+
if permission_summary.get("can_edit_app") is not True:
|
|
2838
|
+
return False
|
|
2839
|
+
tag_ids = _coerce_int_list(permission_summary.get("tag_ids"))
|
|
2840
|
+
for tag_id in tag_ids:
|
|
2841
|
+
try:
|
|
2842
|
+
package_permission = self._read_package_permission_summary(profile=profile, tag_id=tag_id)
|
|
2843
|
+
except (QingflowApiError, RuntimeError):
|
|
2844
|
+
return False
|
|
2845
|
+
if package_permission.get("can_edit_tag") is not True:
|
|
2846
|
+
return False
|
|
2847
|
+
return True
|
|
2848
|
+
|
|
2783
2849
|
def _read_portal_permission_summary(self, *, dash_key: str, portal_result: dict[str, Any]) -> JSONObject:
|
|
2784
2850
|
tag_ids = _coerce_int_list(portal_result.get("tagIds"))
|
|
2785
2851
|
if not tag_ids:
|
|
@@ -2993,7 +3059,7 @@ class AiBuilderFacade:
|
|
|
2993
3059
|
|
|
2994
3060
|
def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
2995
3061
|
try:
|
|
2996
|
-
|
|
3062
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
2997
3063
|
except (QingflowApiError, RuntimeError) as error:
|
|
2998
3064
|
api_error = _coerce_api_error(error)
|
|
2999
3065
|
return _failed_from_api_error(
|
|
@@ -3003,26 +3069,55 @@ class AiBuilderFacade:
|
|
|
3003
3069
|
details={"app_key": app_key},
|
|
3004
3070
|
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3005
3071
|
)
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3072
|
+
base_result = base.get("result") if isinstance(base.get("result"), dict) else {}
|
|
3073
|
+
schema_unavailable = False
|
|
3074
|
+
try:
|
|
3075
|
+
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
3076
|
+
parsed = _parse_schema(schema_result)
|
|
3077
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
3078
|
+
api_error = _coerce_api_error(error)
|
|
3079
|
+
if api_error.http_status == 404 or _is_permission_restricted_api_error(api_error):
|
|
3080
|
+
schema_unavailable = True
|
|
3081
|
+
parsed = {"fields": [], "layout": {"sections": []}}
|
|
3082
|
+
else:
|
|
3083
|
+
return _failed_from_api_error(
|
|
3084
|
+
"APP_READ_FAILED",
|
|
3085
|
+
api_error,
|
|
3086
|
+
normalized_args={"app_key": app_key},
|
|
3087
|
+
details={"app_key": app_key},
|
|
3088
|
+
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3089
|
+
)
|
|
3090
|
+
views, views_unavailable = self._load_views_result(
|
|
3091
|
+
profile=profile,
|
|
3092
|
+
app_key=app_key,
|
|
3093
|
+
tolerate_404=True,
|
|
3094
|
+
tolerate_permission_restricted=True,
|
|
3095
|
+
)
|
|
3096
|
+
workflow, workflow_unavailable = self._load_workflow_result(
|
|
3097
|
+
profile=profile,
|
|
3098
|
+
app_key=app_key,
|
|
3099
|
+
tolerate_404=True,
|
|
3100
|
+
tolerate_permission_restricted=True,
|
|
3101
|
+
)
|
|
3009
3102
|
verification_hints = _build_verification_hints(
|
|
3010
|
-
tag_ids=_coerce_int_list(
|
|
3103
|
+
tag_ids=_coerce_int_list(base_result.get("tagIds")),
|
|
3011
3104
|
fields=parsed["fields"],
|
|
3012
3105
|
layout=parsed["layout"],
|
|
3013
3106
|
views=_summarize_views(views),
|
|
3014
3107
|
)
|
|
3108
|
+
if schema_unavailable:
|
|
3109
|
+
verification_hints.append("schema_read_unavailable")
|
|
3015
3110
|
if views_unavailable:
|
|
3016
3111
|
verification_hints.append("views_read_unavailable")
|
|
3017
3112
|
if workflow_unavailable:
|
|
3018
3113
|
verification_hints.append("workflow_read_unavailable")
|
|
3019
3114
|
response = AppReadSummaryResponse(
|
|
3020
3115
|
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=
|
|
3116
|
+
title=base_result.get("formTitle"),
|
|
3117
|
+
app_icon=str(base_result.get("appIcon") or "").strip() or None,
|
|
3118
|
+
visibility=_public_visibility_from_member_auth(base_result.get("auth")),
|
|
3119
|
+
tag_ids=_coerce_int_list(base_result.get("tagIds")),
|
|
3120
|
+
publish_status=base_result.get("appPublishStatus"),
|
|
3026
3121
|
field_count=len(parsed["fields"]),
|
|
3027
3122
|
layout_section_count=len(parsed["layout"].get("sections", [])),
|
|
3028
3123
|
view_count=len(_summarize_views(views)),
|
|
@@ -3044,10 +3139,11 @@ class AiBuilderFacade:
|
|
|
3044
3139
|
"warnings": _warnings_from_verification_hints(verification_hints),
|
|
3045
3140
|
"verification": {
|
|
3046
3141
|
"app_exists": True,
|
|
3142
|
+
"schema_read_unavailable": schema_unavailable,
|
|
3047
3143
|
"views_read_unavailable": views_unavailable,
|
|
3048
3144
|
"workflow_read_unavailable": workflow_unavailable,
|
|
3049
3145
|
},
|
|
3050
|
-
"verified": not views_unavailable and not workflow_unavailable,
|
|
3146
|
+
"verified": not schema_unavailable and not views_unavailable and not workflow_unavailable,
|
|
3051
3147
|
**response.model_dump(mode="json"),
|
|
3052
3148
|
}
|
|
3053
3149
|
|
|
@@ -3060,8 +3156,9 @@ class AiBuilderFacade:
|
|
|
3060
3156
|
permission_summary = self._read_app_permission_summary(profile=profile, app_key=app_key)
|
|
3061
3157
|
result["message"] = "read app config summary"
|
|
3062
3158
|
result["editability"] = {
|
|
3159
|
+
"can_edit_app_base": self._derive_can_edit_app_base(profile=profile, permission_summary=permission_summary),
|
|
3063
3160
|
"can_edit_form": permission_summary.get("can_edit_app"),
|
|
3064
|
-
"can_edit_flow": permission_summary.get("
|
|
3161
|
+
"can_edit_flow": permission_summary.get("can_manage_data"),
|
|
3065
3162
|
"can_edit_views": permission_summary.get("can_manage_data"),
|
|
3066
3163
|
"can_edit_charts": permission_summary.get("can_manage_data"),
|
|
3067
3164
|
}
|
|
@@ -4630,7 +4727,19 @@ class AiBuilderFacade:
|
|
|
4630
4727
|
)
|
|
4631
4728
|
field = current_fields[matched]
|
|
4632
4729
|
previous_name = field["name"]
|
|
4633
|
-
|
|
4730
|
+
try:
|
|
4731
|
+
_apply_field_mutation(field, patch.set)
|
|
4732
|
+
except ValueError as error:
|
|
4733
|
+
return _failed(
|
|
4734
|
+
"VALIDATION_ERROR",
|
|
4735
|
+
str(error),
|
|
4736
|
+
normalized_args=normalized_args,
|
|
4737
|
+
details={
|
|
4738
|
+
"selector": patch.selector.model_dump(mode="json"),
|
|
4739
|
+
"app_key": target.app_key,
|
|
4740
|
+
},
|
|
4741
|
+
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
4742
|
+
)
|
|
4634
4743
|
current_fields[matched] = field
|
|
4635
4744
|
layout = _rename_field_in_layout(layout, previous_name, field["name"])
|
|
4636
4745
|
updated.append(field["name"])
|
|
@@ -4789,12 +4898,22 @@ class AiBuilderFacade:
|
|
|
4789
4898
|
response = _apply_permission_outcomes(response, relation_permission_outcome)
|
|
4790
4899
|
return finalize(self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response))
|
|
4791
4900
|
|
|
4792
|
-
payload =
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4901
|
+
payload = (
|
|
4902
|
+
_build_form_payload_from_fields(
|
|
4903
|
+
title=effective_app_name,
|
|
4904
|
+
current_schema=schema_result,
|
|
4905
|
+
fields=current_fields,
|
|
4906
|
+
layout=layout,
|
|
4907
|
+
question_relations=compiled_question_relations,
|
|
4908
|
+
)
|
|
4909
|
+
if bool(resolved.get("created"))
|
|
4910
|
+
else _build_form_payload_for_edit_fields(
|
|
4911
|
+
title=effective_app_name,
|
|
4912
|
+
current_schema=schema_result,
|
|
4913
|
+
fields=current_fields,
|
|
4914
|
+
layout=layout,
|
|
4915
|
+
question_relations=compiled_question_relations,
|
|
4916
|
+
)
|
|
4798
4917
|
)
|
|
4799
4918
|
payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
4800
4919
|
profile=profile,
|
|
@@ -4897,12 +5016,22 @@ class AiBuilderFacade:
|
|
|
4897
5016
|
},
|
|
4898
5017
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
4899
5018
|
)
|
|
4900
|
-
rebound_payload =
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
5019
|
+
rebound_payload = (
|
|
5020
|
+
_build_form_payload_from_fields(
|
|
5021
|
+
title=effective_app_name,
|
|
5022
|
+
current_schema=rebound_schema,
|
|
5023
|
+
fields=rebound_fields,
|
|
5024
|
+
layout=rebound_layout,
|
|
5025
|
+
question_relations=compiled_question_relations,
|
|
5026
|
+
)
|
|
5027
|
+
if bool(resolved.get("created"))
|
|
5028
|
+
else _build_form_payload_for_edit_fields(
|
|
5029
|
+
title=effective_app_name,
|
|
5030
|
+
current_schema=rebound_schema,
|
|
5031
|
+
fields=rebound_fields,
|
|
5032
|
+
layout=rebound_layout,
|
|
5033
|
+
question_relations=compiled_question_relations,
|
|
5034
|
+
)
|
|
4906
5035
|
)
|
|
4907
5036
|
rebound_payload["editVersionNo"] = self._resolve_form_edit_version(
|
|
4908
5037
|
profile=profile,
|
|
@@ -6670,6 +6799,7 @@ class AiBuilderFacade:
|
|
|
6670
6799
|
|
|
6671
6800
|
for patch in request.upsert_charts:
|
|
6672
6801
|
try:
|
|
6802
|
+
config_update_requested = _chart_patch_updates_chart_config(patch)
|
|
6673
6803
|
chart_visible_auth = (
|
|
6674
6804
|
self._compile_visibility_to_chart_visible_auth(profile=profile, visibility=patch.visibility)
|
|
6675
6805
|
if patch.visibility is not None
|
|
@@ -6773,18 +6903,17 @@ class AiBuilderFacade:
|
|
|
6773
6903
|
existing_by_name.pop(old_name, None)
|
|
6774
6904
|
existing_by_name.setdefault(patch.name, []).append(deepcopy(updated_chart))
|
|
6775
6905
|
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
if existing is not None and chart_id not in updated_ids:
|
|
6906
|
+
config_updated = False
|
|
6907
|
+
if existing is None or config_update_requested:
|
|
6908
|
+
config_payload = _build_public_chart_config_payload(
|
|
6909
|
+
patch=patch,
|
|
6910
|
+
app_key=app_key,
|
|
6911
|
+
field_lookup=field_lookup,
|
|
6912
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
6913
|
+
)
|
|
6914
|
+
self.charts.qingbi_report_update_config(profile=profile, chart_id=chart_id, payload=config_payload)
|
|
6915
|
+
config_updated = True
|
|
6916
|
+
if existing is not None and chart_id not in updated_ids and config_updated:
|
|
6788
6917
|
updated_ids.append(chart_id)
|
|
6789
6918
|
if patch.question_config:
|
|
6790
6919
|
self._request_backend(
|
|
@@ -6800,6 +6929,8 @@ class AiBuilderFacade:
|
|
|
6800
6929
|
path=f"/chart/{chart_id}/user/config",
|
|
6801
6930
|
json_body=patch.user_config,
|
|
6802
6931
|
)
|
|
6932
|
+
if existing is not None and chart_id not in updated_ids and (patch.question_config or patch.user_config):
|
|
6933
|
+
updated_ids.append(chart_id)
|
|
6803
6934
|
chart_results.append(
|
|
6804
6935
|
{
|
|
6805
6936
|
"chart_id": chart_id,
|
|
@@ -6956,11 +7087,12 @@ class AiBuilderFacade:
|
|
|
6956
7087
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
6957
7088
|
dash_key = str(request.dash_key or "").strip()
|
|
6958
7089
|
creating = not dash_key
|
|
7090
|
+
sections_requested = creating or bool(request.sections)
|
|
6959
7091
|
verify_dash_name = creating or request.dash_name is not None
|
|
6960
7092
|
verify_dash_icon = bool(request.icon or request.color)
|
|
6961
7093
|
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
|
|
7094
|
+
verify_hide_copyright = request.hide_copyright is not None and sections_requested
|
|
7095
|
+
verify_dash_global_config = request.dash_global_config is not None and sections_requested
|
|
6964
7096
|
verify_tags = creating or request.package_tag_id is not None
|
|
6965
7097
|
requested_visibility = request.visibility
|
|
6966
7098
|
if requested_visibility is None and isinstance(request.auth, dict) and request.auth:
|
|
@@ -7037,6 +7169,25 @@ class AiBuilderFacade:
|
|
|
7037
7169
|
if package_edit_outcome.block is not None:
|
|
7038
7170
|
return package_edit_outcome.block
|
|
7039
7171
|
permission_outcomes.append(package_edit_outcome)
|
|
7172
|
+
if not sections_requested:
|
|
7173
|
+
unsupported_base_only_keys: list[str] = []
|
|
7174
|
+
if request.hide_copyright is not None:
|
|
7175
|
+
unsupported_base_only_keys.append("hide_copyright")
|
|
7176
|
+
if request.dash_global_config is not None:
|
|
7177
|
+
unsupported_base_only_keys.append("dash_global_config")
|
|
7178
|
+
if request.config:
|
|
7179
|
+
unsupported_base_only_keys.append("config")
|
|
7180
|
+
if unsupported_base_only_keys:
|
|
7181
|
+
return _failed(
|
|
7182
|
+
"PORTAL_SECTIONS_REQUIRED",
|
|
7183
|
+
"editing a portal without sections only supports base-info updates",
|
|
7184
|
+
normalized_args=normalized_args,
|
|
7185
|
+
details={
|
|
7186
|
+
"unsupported_without_sections": unsupported_base_only_keys,
|
|
7187
|
+
"fix_hint": "Pass sections when changing layout or global portal config, or omit those keys for visibility/icon/package updates.",
|
|
7188
|
+
},
|
|
7189
|
+
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7190
|
+
)
|
|
7040
7191
|
try:
|
|
7041
7192
|
if creating:
|
|
7042
7193
|
create_payload = _build_public_portal_base_payload(
|
|
@@ -7062,7 +7213,6 @@ class AiBuilderFacade:
|
|
|
7062
7213
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7063
7214
|
)
|
|
7064
7215
|
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
7216
|
update_payload = _build_public_portal_base_payload(
|
|
7067
7217
|
dash_name=request.dash_name or str(base_payload.get("dashName") or "").strip() or "未命名门户",
|
|
7068
7218
|
package_tag_id=target_package_tag_id,
|
|
@@ -7074,8 +7224,10 @@ class AiBuilderFacade:
|
|
|
7074
7224
|
config=request.config,
|
|
7075
7225
|
base_payload=base_payload,
|
|
7076
7226
|
)
|
|
7077
|
-
|
|
7078
|
-
|
|
7227
|
+
if sections_requested:
|
|
7228
|
+
component_payload = self._build_portal_components_from_sections(profile=profile, sections=request.sections)
|
|
7229
|
+
update_payload["components"] = component_payload
|
|
7230
|
+
self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
7079
7231
|
self.portals.portal_update_base_info(
|
|
7080
7232
|
profile=profile,
|
|
7081
7233
|
dash_key=dash_key,
|
|
@@ -7112,11 +7264,14 @@ class AiBuilderFacade:
|
|
|
7112
7264
|
publish_failed = True
|
|
7113
7265
|
|
|
7114
7266
|
draft_components = draft_result.get("components") if isinstance(draft_result, dict) else None
|
|
7115
|
-
expected_count = len(request.sections)
|
|
7116
|
-
draft_verified = isinstance(
|
|
7267
|
+
expected_count = len(request.sections) if sections_requested else None
|
|
7268
|
+
draft_verified = isinstance(draft_result, dict) and (
|
|
7269
|
+
expected_count is None or (isinstance(draft_components, list) and len(draft_components) == expected_count)
|
|
7270
|
+
)
|
|
7117
7271
|
draft_meta_verified, draft_meta_mismatches = _verify_portal_readback(
|
|
7118
7272
|
actual=draft_result,
|
|
7119
7273
|
expected_payload=update_payload,
|
|
7274
|
+
expected_visibility=requested_visibility.model_dump(mode="json") if requested_visibility is not None else None,
|
|
7120
7275
|
expected_section_count=expected_count,
|
|
7121
7276
|
requested_config_keys=set((request.config or {}).keys()),
|
|
7122
7277
|
verify_dash_name=verify_dash_name,
|
|
@@ -7132,12 +7287,18 @@ class AiBuilderFacade:
|
|
|
7132
7287
|
if request.publish:
|
|
7133
7288
|
live_verified = (
|
|
7134
7289
|
isinstance(live_result, dict)
|
|
7135
|
-
and
|
|
7136
|
-
|
|
7290
|
+
and (
|
|
7291
|
+
expected_count is None
|
|
7292
|
+
or (
|
|
7293
|
+
isinstance(live_result.get("components"), list)
|
|
7294
|
+
and len(live_result.get("components")) == expected_count
|
|
7295
|
+
)
|
|
7296
|
+
)
|
|
7137
7297
|
)
|
|
7138
7298
|
live_meta_verified, live_meta_mismatches = _verify_portal_readback(
|
|
7139
7299
|
actual=live_result,
|
|
7140
7300
|
expected_payload=update_payload,
|
|
7301
|
+
expected_visibility=requested_visibility.model_dump(mode="json") if requested_visibility is not None else None,
|
|
7141
7302
|
expected_section_count=expected_count,
|
|
7142
7303
|
requested_config_keys=set((request.config or {}).keys()),
|
|
7143
7304
|
verify_dash_name=verify_dash_name,
|
|
@@ -7171,7 +7332,15 @@ class AiBuilderFacade:
|
|
|
7171
7332
|
"status": status,
|
|
7172
7333
|
"error_code": error_code,
|
|
7173
7334
|
"recoverable": not verified,
|
|
7174
|
-
"message":
|
|
7335
|
+
"message": (
|
|
7336
|
+
"updated portal base info"
|
|
7337
|
+
if verified and not sections_requested
|
|
7338
|
+
else "applied portal"
|
|
7339
|
+
if verified
|
|
7340
|
+
else "updated portal base info; draft/live verification pending"
|
|
7341
|
+
if not sections_requested
|
|
7342
|
+
else "applied portal; draft/live verification pending"
|
|
7343
|
+
),
|
|
7175
7344
|
"normalized_args": normalized_args,
|
|
7176
7345
|
"missing_fields": [],
|
|
7177
7346
|
"allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"]},
|
|
@@ -7366,17 +7535,35 @@ class AiBuilderFacade:
|
|
|
7366
7535
|
sync_result = {**sync_result, "button_config_restored": True}
|
|
7367
7536
|
return sync_result
|
|
7368
7537
|
|
|
7369
|
-
def _load_views_result(
|
|
7538
|
+
def _load_views_result(
|
|
7539
|
+
self,
|
|
7540
|
+
*,
|
|
7541
|
+
profile: str,
|
|
7542
|
+
app_key: str,
|
|
7543
|
+
tolerate_404: bool,
|
|
7544
|
+
tolerate_permission_restricted: bool = False,
|
|
7545
|
+
) -> tuple[Any, bool]:
|
|
7370
7546
|
try:
|
|
7371
7547
|
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
7372
7548
|
except (QingflowApiError, RuntimeError) as error:
|
|
7373
7549
|
api_error = _coerce_api_error(error)
|
|
7374
|
-
if api_error.http_status == 404
|
|
7550
|
+
if api_error.http_status == 404 or (
|
|
7551
|
+
tolerate_permission_restricted and _is_permission_restricted_api_error(api_error)
|
|
7552
|
+
):
|
|
7375
7553
|
try:
|
|
7376
7554
|
legacy_views = self.views.view_list(profile=profile, app_key=app_key)
|
|
7377
7555
|
except (QingflowApiError, RuntimeError) as legacy_error:
|
|
7378
7556
|
legacy_api_error = _coerce_api_error(legacy_error)
|
|
7379
|
-
if
|
|
7557
|
+
if (
|
|
7558
|
+
tolerate_404
|
|
7559
|
+
and (
|
|
7560
|
+
legacy_api_error.http_status == 404
|
|
7561
|
+
or (
|
|
7562
|
+
tolerate_permission_restricted
|
|
7563
|
+
and _is_permission_restricted_api_error(legacy_api_error)
|
|
7564
|
+
)
|
|
7565
|
+
)
|
|
7566
|
+
):
|
|
7380
7567
|
return [], True
|
|
7381
7568
|
raise
|
|
7382
7569
|
legacy_result = legacy_views.get("result")
|
|
@@ -7393,19 +7580,38 @@ class AiBuilderFacade:
|
|
|
7393
7580
|
legacy_views = self.views.view_list(profile=profile, app_key=app_key)
|
|
7394
7581
|
except (QingflowApiError, RuntimeError) as legacy_error:
|
|
7395
7582
|
legacy_api_error = _coerce_api_error(legacy_error)
|
|
7396
|
-
if
|
|
7583
|
+
if (
|
|
7584
|
+
tolerate_404
|
|
7585
|
+
and (
|
|
7586
|
+
legacy_api_error.http_status == 404
|
|
7587
|
+
or (
|
|
7588
|
+
tolerate_permission_restricted
|
|
7589
|
+
and _is_permission_restricted_api_error(legacy_api_error)
|
|
7590
|
+
)
|
|
7591
|
+
)
|
|
7592
|
+
):
|
|
7397
7593
|
return normalized_views, False
|
|
7398
7594
|
raise
|
|
7399
7595
|
legacy_result = legacy_views.get("result")
|
|
7400
7596
|
legacy_normalized = _normalize_view_collection(legacy_result)
|
|
7401
7597
|
return legacy_normalized or normalized_views, False
|
|
7402
7598
|
|
|
7403
|
-
def _load_workflow_result(
|
|
7599
|
+
def _load_workflow_result(
|
|
7600
|
+
self,
|
|
7601
|
+
*,
|
|
7602
|
+
profile: str,
|
|
7603
|
+
app_key: str,
|
|
7604
|
+
tolerate_404: bool,
|
|
7605
|
+
tolerate_permission_restricted: bool = False,
|
|
7606
|
+
) -> tuple[Any, bool]:
|
|
7404
7607
|
try:
|
|
7405
7608
|
workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
7406
7609
|
except (QingflowApiError, RuntimeError) as error:
|
|
7407
7610
|
api_error = _coerce_api_error(error)
|
|
7408
|
-
if tolerate_404 and
|
|
7611
|
+
if tolerate_404 and (
|
|
7612
|
+
api_error.http_status == 404
|
|
7613
|
+
or (tolerate_permission_restricted and _is_permission_restricted_api_error(api_error))
|
|
7614
|
+
):
|
|
7409
7615
|
return [], True
|
|
7410
7616
|
raise
|
|
7411
7617
|
return workflow.get("result"), False
|
|
@@ -7850,8 +8056,6 @@ def _serialize_custom_button_payload(payload: CustomButtonPatch) -> dict[str, An
|
|
|
7850
8056
|
"buttonIcon": data["button_icon"],
|
|
7851
8057
|
"triggerAction": data["trigger_action"],
|
|
7852
8058
|
}
|
|
7853
|
-
if str(data.get("icon_color") or "").strip():
|
|
7854
|
-
serialized["iconColor"] = data["icon_color"]
|
|
7855
8059
|
if str(data.get("trigger_link_url") or "").strip():
|
|
7856
8060
|
serialized["triggerLinkUrl"] = data["trigger_link_url"]
|
|
7857
8061
|
trigger_add_data_config = data.get("trigger_add_data_config")
|
|
@@ -7941,7 +8145,6 @@ def _normalize_custom_button_summary(item: dict[str, Any]) -> dict[str, Any]:
|
|
|
7941
8145
|
"button_id": _coerce_positive_int(item.get("button_id") or item.get("buttonId") or item.get("id")),
|
|
7942
8146
|
"button_text": str(item.get("button_text") or item.get("buttonText") or "").strip() or None,
|
|
7943
8147
|
"button_icon": str(item.get("button_icon") or item.get("buttonIcon") or "").strip() or None,
|
|
7944
|
-
"icon_color": str(item.get("icon_color") or item.get("iconColor") or "").strip() or None,
|
|
7945
8148
|
"background_color": str(item.get("background_color") or item.get("backgroundColor") or "").strip() or None,
|
|
7946
8149
|
"text_color": str(item.get("text_color") or item.get("textColor") or "").strip() or None,
|
|
7947
8150
|
"used_in_chart_count": _coerce_nonnegative_int(item.get("used_in_chart_count") or item.get("userInChartCount")),
|
|
@@ -8697,6 +8900,11 @@ def _build_public_chart_config_payload(
|
|
|
8697
8900
|
return payload
|
|
8698
8901
|
|
|
8699
8902
|
|
|
8903
|
+
def _chart_patch_updates_chart_config(patch: ChartUpsertPatch) -> bool:
|
|
8904
|
+
explicit_fields = set(getattr(patch, "model_fields_set", set()) or set())
|
|
8905
|
+
return bool({"dimension_field_ids", "indicator_field_ids", "filters", "config"} & explicit_fields)
|
|
8906
|
+
|
|
8907
|
+
|
|
8700
8908
|
def _build_public_portal_base_payload(
|
|
8701
8909
|
*,
|
|
8702
8910
|
dash_name: str,
|
|
@@ -8926,7 +9134,8 @@ def _verify_portal_readback(
|
|
|
8926
9134
|
*,
|
|
8927
9135
|
actual: Any,
|
|
8928
9136
|
expected_payload: dict[str, Any],
|
|
8929
|
-
|
|
9137
|
+
expected_visibility: dict[str, Any] | None,
|
|
9138
|
+
expected_section_count: int | None,
|
|
8930
9139
|
requested_config_keys: set[str],
|
|
8931
9140
|
verify_dash_name: bool,
|
|
8932
9141
|
verify_dash_icon: bool,
|
|
@@ -8939,14 +9148,19 @@ def _verify_portal_readback(
|
|
|
8939
9148
|
if not isinstance(actual, dict):
|
|
8940
9149
|
return False, ["portal readback payload is unavailable"]
|
|
8941
9150
|
components = actual.get("components")
|
|
8942
|
-
if not isinstance(components, list) or len(components) != expected_section_count:
|
|
9151
|
+
if expected_section_count is not None and (not isinstance(components, list) or len(components) != expected_section_count):
|
|
8943
9152
|
mismatches.append(f"components expected {expected_section_count}, got {len(components) if isinstance(components, list) else 'unavailable'}")
|
|
8944
9153
|
if verify_dash_name and str(actual.get("dashName") or "").strip() != str(expected_payload.get("dashName") or "").strip():
|
|
8945
9154
|
mismatches.append("dash_name")
|
|
8946
9155
|
if verify_dash_icon and str(actual.get("dashIcon") or "") != str(expected_payload.get("dashIcon") or ""):
|
|
8947
9156
|
mismatches.append("dash_icon")
|
|
8948
|
-
if verify_auth
|
|
8949
|
-
|
|
9157
|
+
if verify_auth:
|
|
9158
|
+
if expected_visibility is not None:
|
|
9159
|
+
actual_visibility = _public_visibility_from_member_auth(actual.get("auth"))
|
|
9160
|
+
if not _visibility_matches_expected(actual_visibility, expected_visibility):
|
|
9161
|
+
mismatches.append("auth")
|
|
9162
|
+
elif not _mapping_contains(actual.get("auth"), expected_payload.get("auth")):
|
|
9163
|
+
mismatches.append("auth")
|
|
8950
9164
|
if verify_hide_copyright and bool(actual.get("hideCopyright", False)) != bool(expected_payload.get("hideCopyright", False)):
|
|
8951
9165
|
mismatches.append("hide_copyright")
|
|
8952
9166
|
if verify_dash_global_config and not _mapping_contains(actual.get("dashGlobalConfig") or {}, expected_payload.get("dashGlobalConfig") or {}):
|
|
@@ -9154,7 +9368,11 @@ def _visibility_matches_expected(actual: Any, expected: Any) -> bool:
|
|
|
9154
9368
|
if expected_text and sorted_values(actual_group, text_key) != expected_text:
|
|
9155
9369
|
return False
|
|
9156
9370
|
|
|
9157
|
-
if
|
|
9371
|
+
if (
|
|
9372
|
+
"include_sub_departs" in expected_selectors
|
|
9373
|
+
and expected_selectors.get("include_sub_departs") is not None
|
|
9374
|
+
and actual_selectors.get("include_sub_departs") != expected_selectors.get("include_sub_departs")
|
|
9375
|
+
):
|
|
9158
9376
|
return False
|
|
9159
9377
|
return True
|
|
9160
9378
|
|
|
@@ -9611,6 +9829,15 @@ def _apply_relation_target_selection(
|
|
|
9611
9829
|
config["refer_field_types"] = [item.get("type") for item in normalized_visible]
|
|
9612
9830
|
config["auth_field_ids"] = [item.get("field_id") or item.get("name") for item in normalized_visible]
|
|
9613
9831
|
config["auth_field_que_ids"] = [_coerce_positive_int(item.get("que_id")) or 0 for item in normalized_visible]
|
|
9832
|
+
config["refer_auth_ques"] = [
|
|
9833
|
+
{
|
|
9834
|
+
"queId": _coerce_positive_int(item.get("que_id")) or 0,
|
|
9835
|
+
"queAuth": _REFERENCE_FIELD_VISIBLE_AUTH,
|
|
9836
|
+
"_field_id": item.get("field_id") or item.get("name"),
|
|
9837
|
+
}
|
|
9838
|
+
for item in normalized_visible
|
|
9839
|
+
if (_coerce_positive_int(item.get("que_id")) or 0) > 0
|
|
9840
|
+
]
|
|
9614
9841
|
config["field_name_show"] = bool(field.get("field_name_show", True))
|
|
9615
9842
|
field["target_field_id"] = display_field.get("field_id") or display_field.get("name")
|
|
9616
9843
|
field["target_field_que_id"] = _coerce_positive_int(display_field.get("que_id")) or 0
|
|
@@ -9844,9 +10071,10 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
9844
10071
|
"subfields": [],
|
|
9845
10072
|
"que_id": que_id,
|
|
9846
10073
|
"que_type": que_type,
|
|
9847
|
-
"default_type": _coerce_positive_int(question.get("queDefaultType"))
|
|
9848
|
-
"default_value": question.get("queDefaultValue"),
|
|
10074
|
+
"default_type": _coerce_positive_int(question.get("queDefaultType")) if "queDefaultType" in question else None,
|
|
9849
10075
|
}
|
|
10076
|
+
if "queDefaultValue" in question:
|
|
10077
|
+
field["default_value"] = question.get("queDefaultValue")
|
|
9850
10078
|
if field_type in {FieldType.single_select.value, FieldType.multi_select.value, FieldType.boolean.value}:
|
|
9851
10079
|
options = question.get("options")
|
|
9852
10080
|
if isinstance(options, list):
|
|
@@ -9870,17 +10098,32 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
9870
10098
|
field["target_app_key"] = reference.get("referAppKey")
|
|
9871
10099
|
field["relation_mode"] = _relation_mode_from_optional_data_num(reference.get("optionalDataNum"))
|
|
9872
10100
|
refer_questions = reference.get("referQuestions") if isinstance(reference.get("referQuestions"), list) else []
|
|
10101
|
+
refer_auth_questions = reference.get("referAuthQues") if isinstance(reference.get("referAuthQues"), list) else []
|
|
10102
|
+
refer_auth_by_que_id: dict[int, int] = {}
|
|
10103
|
+
for raw_item in refer_auth_questions:
|
|
10104
|
+
if not isinstance(raw_item, dict):
|
|
10105
|
+
continue
|
|
10106
|
+
que_id = _coerce_nonnegative_int(raw_item.get("queId"))
|
|
10107
|
+
que_auth = _coerce_nonnegative_int(raw_item.get("queAuth"))
|
|
10108
|
+
if que_id is None or que_auth is None or que_id in refer_auth_by_que_id:
|
|
10109
|
+
continue
|
|
10110
|
+
refer_auth_by_que_id[que_id] = que_auth
|
|
9873
10111
|
visible_fields: list[dict[str, Any]] = []
|
|
9874
10112
|
display_field_que_id = _coerce_nonnegative_int(reference.get("referQueId"))
|
|
9875
10113
|
display_field_name: str | None = None
|
|
9876
10114
|
for item in refer_questions:
|
|
9877
10115
|
if not isinstance(item, dict):
|
|
9878
10116
|
continue
|
|
10117
|
+
que_id = _coerce_nonnegative_int(item.get("queId"))
|
|
10118
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
10119
|
+
if que_auth is None and que_id is not None:
|
|
10120
|
+
que_auth = refer_auth_by_que_id.get(que_id)
|
|
9879
10121
|
selector = {
|
|
9880
|
-
"que_id":
|
|
10122
|
+
"que_id": que_id,
|
|
9881
10123
|
"name": str(item.get("queTitle") or "").strip() or None,
|
|
9882
10124
|
}
|
|
9883
|
-
|
|
10125
|
+
if que_auth != _REFERENCE_FIELD_HIDDEN_AUTH:
|
|
10126
|
+
visible_fields.append(selector)
|
|
9884
10127
|
if display_field_que_id is not None and selector["que_id"] == display_field_que_id:
|
|
9885
10128
|
display_field_name = selector["name"]
|
|
9886
10129
|
if display_field_name is None and visible_fields:
|
|
@@ -9960,6 +10203,7 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
9960
10203
|
continue
|
|
9961
10204
|
subfields.append(_parse_field(sub_question))
|
|
9962
10205
|
field["subfields"] = subfields
|
|
10206
|
+
field["_question_template"] = deepcopy(question)
|
|
9963
10207
|
return field
|
|
9964
10208
|
|
|
9965
10209
|
|
|
@@ -10505,14 +10749,14 @@ def _parse_code_block_inputs_and_body(code_content: str) -> tuple[list[dict[str,
|
|
|
10505
10749
|
return inputs, body
|
|
10506
10750
|
|
|
10507
10751
|
|
|
10508
|
-
def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any]) -> dict[str, Any]:
|
|
10752
|
+
def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any], nested: bool = False) -> dict[str, Any]:
|
|
10509
10753
|
payload = {
|
|
10510
10754
|
"field_id": field.get("field_id"),
|
|
10511
10755
|
"que_id": field.get("que_id"),
|
|
10512
10756
|
"name": field.get("name"),
|
|
10513
10757
|
"type": field.get("type"),
|
|
10514
10758
|
"required": bool(field.get("required")),
|
|
10515
|
-
"section_id": _find_field_section_id(layout, str(field.get("name") or "")),
|
|
10759
|
+
"section_id": None if nested else _find_field_section_id(layout, str(field.get("name") or "")),
|
|
10516
10760
|
}
|
|
10517
10761
|
if field.get("type") == FieldType.relation.value:
|
|
10518
10762
|
payload["target_app_key"] = field.get("target_app_key")
|
|
@@ -10545,6 +10789,12 @@ def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any])
|
|
|
10545
10789
|
payload["custom_button_text"] = field.get("custom_button_text")
|
|
10546
10790
|
if field.get("metadata_unverified") is not None:
|
|
10547
10791
|
payload["metadata_unverified"] = bool(field.get("metadata_unverified"))
|
|
10792
|
+
if field.get("type") == FieldType.subtable.value:
|
|
10793
|
+
payload["subfields"] = [
|
|
10794
|
+
_compact_public_field_read(field=subfield, layout=layout, nested=True)
|
|
10795
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or [])
|
|
10796
|
+
if isinstance(subfield, dict)
|
|
10797
|
+
]
|
|
10548
10798
|
return payload
|
|
10549
10799
|
|
|
10550
10800
|
|
|
@@ -10840,59 +11090,119 @@ def _code_block_binding_equal(left: Any, right: Any) -> bool:
|
|
|
10840
11090
|
return _normalize_code_block_binding(left) == _normalize_code_block_binding(right)
|
|
10841
11091
|
|
|
10842
11092
|
|
|
11093
|
+
_SAFE_SUBFIELD_MUTATION_KEYS = frozenset({"name", "required", "description", "subfield_updates"})
|
|
11094
|
+
|
|
11095
|
+
|
|
11096
|
+
def _validate_safe_subfield_mutation(*, payload: dict[str, Any], location: str) -> None:
|
|
11097
|
+
unsupported = sorted(key for key in payload if key not in _SAFE_SUBFIELD_MUTATION_KEYS)
|
|
11098
|
+
if unsupported:
|
|
11099
|
+
raise ValueError(
|
|
11100
|
+
f"{location} only supports safe overlay keys: name, required, description, subfield_updates; "
|
|
11101
|
+
f"unsupported keys: {', '.join(unsupported)}"
|
|
11102
|
+
)
|
|
11103
|
+
|
|
11104
|
+
|
|
11105
|
+
def _apply_subfield_updates(field: dict[str, Any], raw_updates: list[Any]) -> None:
|
|
11106
|
+
if str(field.get("type") or "") != FieldType.subtable.value:
|
|
11107
|
+
raise ValueError("subfield_updates can only target subtable fields")
|
|
11108
|
+
subfields = [subfield for subfield in cast(list[dict[str, Any]], field.get("subfields") or []) if isinstance(subfield, dict)]
|
|
11109
|
+
for index, raw_item in enumerate(raw_updates):
|
|
11110
|
+
patch = FieldUpdatePatch.model_validate(raw_item)
|
|
11111
|
+
payload = patch.set.model_dump(mode="json", exclude_none=True)
|
|
11112
|
+
_validate_safe_subfield_mutation(payload=payload, location=f"subfield_updates[{index}].set")
|
|
11113
|
+
target = _resolve_field_selector_with_uniqueness(
|
|
11114
|
+
fields=subfields,
|
|
11115
|
+
selector_payload=patch.selector.model_dump(mode="json", exclude_none=True),
|
|
11116
|
+
location=f"subfield_updates[{index}].selector",
|
|
11117
|
+
)
|
|
11118
|
+
_apply_field_mutation(target, patch.set)
|
|
11119
|
+
field["subfields"] = subfields
|
|
11120
|
+
|
|
11121
|
+
|
|
10843
11122
|
def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
10844
11123
|
payload = mutation.model_dump(mode="json", exclude_none=True)
|
|
10845
11124
|
relation_config_explicit = (
|
|
10846
11125
|
payload.get("type") == FieldType.relation.value
|
|
10847
11126
|
or any(key in payload for key in ("target_app_key", "display_field", "visible_fields", "relation_mode"))
|
|
10848
11127
|
)
|
|
11128
|
+
question_overlay_keys = set(cast(list[str], field.get("_question_overlay_keys") or []))
|
|
11129
|
+
question_rebuild_required = bool(field.get("_question_rebuild_required"))
|
|
10849
11130
|
if "name" in payload:
|
|
10850
11131
|
field["name"] = payload["name"]
|
|
11132
|
+
question_overlay_keys.add("name")
|
|
10851
11133
|
if "type" in payload:
|
|
10852
11134
|
field["type"] = payload["type"]
|
|
11135
|
+
question_rebuild_required = True
|
|
10853
11136
|
if "required" in payload:
|
|
10854
11137
|
field["required"] = payload["required"]
|
|
11138
|
+
question_overlay_keys.add("required")
|
|
10855
11139
|
if "description" in payload:
|
|
10856
11140
|
field["description"] = payload["description"]
|
|
11141
|
+
question_overlay_keys.add("description")
|
|
10857
11142
|
if "options" in payload:
|
|
10858
11143
|
field["options"] = list(payload["options"])
|
|
11144
|
+
question_rebuild_required = True
|
|
10859
11145
|
if "target_app_key" in payload:
|
|
10860
11146
|
field["target_app_key"] = payload["target_app_key"]
|
|
11147
|
+
question_rebuild_required = True
|
|
10861
11148
|
if "display_field" in payload:
|
|
10862
11149
|
field["display_field"] = payload["display_field"]
|
|
11150
|
+
question_rebuild_required = True
|
|
10863
11151
|
if "visible_fields" in payload:
|
|
10864
11152
|
field["visible_fields"] = list(payload["visible_fields"])
|
|
11153
|
+
question_rebuild_required = True
|
|
10865
11154
|
if "relation_mode" in payload:
|
|
10866
11155
|
field["relation_mode"] = payload["relation_mode"]
|
|
11156
|
+
question_rebuild_required = True
|
|
10867
11157
|
if "department_scope" in payload:
|
|
10868
11158
|
field["department_scope"] = payload["department_scope"]
|
|
11159
|
+
question_rebuild_required = True
|
|
10869
11160
|
if "remote_lookup_config" in payload:
|
|
10870
11161
|
field["remote_lookup_config"] = payload["remote_lookup_config"]
|
|
10871
11162
|
field["config"] = deepcopy(payload["remote_lookup_config"])
|
|
10872
11163
|
field["_explicit_remote_lookup_config"] = True
|
|
11164
|
+
question_rebuild_required = True
|
|
10873
11165
|
if "q_linker_binding" in payload:
|
|
10874
11166
|
field["q_linker_binding"] = payload["q_linker_binding"]
|
|
10875
11167
|
if "remote_lookup_config" not in payload:
|
|
10876
11168
|
field["_explicit_remote_lookup_config"] = False
|
|
11169
|
+
question_rebuild_required = True
|
|
10877
11170
|
if "code_block_config" in payload:
|
|
10878
11171
|
field["code_block_config"] = payload["code_block_config"]
|
|
10879
11172
|
field["config"] = deepcopy(payload["code_block_config"])
|
|
11173
|
+
question_rebuild_required = True
|
|
10880
11174
|
if "code_block_binding" in payload:
|
|
10881
11175
|
field["code_block_binding"] = payload["code_block_binding"]
|
|
10882
11176
|
field["_explicit_code_block_binding"] = True
|
|
11177
|
+
question_rebuild_required = True
|
|
10883
11178
|
if "auto_trigger" in payload:
|
|
10884
11179
|
field["auto_trigger"] = payload["auto_trigger"]
|
|
11180
|
+
question_rebuild_required = True
|
|
10885
11181
|
if "custom_button_text_enabled" in payload:
|
|
10886
11182
|
field["custom_button_text_enabled"] = payload["custom_button_text_enabled"]
|
|
11183
|
+
question_rebuild_required = True
|
|
10887
11184
|
if "custom_button_text" in payload:
|
|
10888
11185
|
field["custom_button_text"] = payload["custom_button_text"]
|
|
11186
|
+
question_rebuild_required = True
|
|
10889
11187
|
if "subfields" in payload:
|
|
10890
11188
|
field["subfields"] = [_field_patch_to_internal(item) for item in payload["subfields"]]
|
|
11189
|
+
question_rebuild_required = True
|
|
11190
|
+
if "subfield_updates" in payload:
|
|
11191
|
+
_apply_subfield_updates(field, payload["subfield_updates"])
|
|
10891
11192
|
if relation_config_explicit:
|
|
10892
11193
|
field["_relation_config_explicit"] = True
|
|
11194
|
+
question_rebuild_required = True
|
|
10893
11195
|
elif payload.get("type") and payload.get("type") != FieldType.relation.value:
|
|
10894
11196
|
field.pop("_relation_config_explicit", None)
|
|
10895
11197
|
field.pop("_reference_config_template", None)
|
|
11198
|
+
if question_overlay_keys:
|
|
11199
|
+
field["_question_overlay_keys"] = sorted(question_overlay_keys)
|
|
11200
|
+
else:
|
|
11201
|
+
field.pop("_question_overlay_keys", None)
|
|
11202
|
+
if question_rebuild_required:
|
|
11203
|
+
field["_question_rebuild_required"] = True
|
|
11204
|
+
else:
|
|
11205
|
+
field.pop("_question_rebuild_required", None)
|
|
10896
11206
|
|
|
10897
11207
|
|
|
10898
11208
|
def _resolve_field_selector_with_uniqueness(
|
|
@@ -12080,6 +12390,7 @@ def _warnings_from_verification_hints(hints: list[str]) -> list[dict[str, Any]]:
|
|
|
12080
12390
|
"package attachment not verified": _warning("PACKAGE_ATTACHMENT_UNVERIFIED", "package attachment is not verified"),
|
|
12081
12391
|
"layout has unplaced fields": _warning("LAYOUT_HAS_UNPLACED_FIELDS", "layout still contains unplaced fields"),
|
|
12082
12392
|
"no public views detected": _warning("NO_PUBLIC_VIEWS", "no public views were detected"),
|
|
12393
|
+
"schema_read_unavailable": _warning("SCHEMA_READ_UNAVAILABLE", "schema summary readback is unavailable"),
|
|
12083
12394
|
"views_read_unavailable": _warning("VIEWS_READ_UNAVAILABLE", "views summary readback is unavailable"),
|
|
12084
12395
|
"workflow_read_unavailable": _warning("WORKFLOW_READ_UNAVAILABLE", "workflow summary readback is unavailable"),
|
|
12085
12396
|
}
|
|
@@ -12380,6 +12691,26 @@ def _public_package_items_from_tag_items(tag_items: Any) -> list[JSONObject]:
|
|
|
12380
12691
|
return public_items
|
|
12381
12692
|
|
|
12382
12693
|
|
|
12694
|
+
def _select_package_layout_tag_items(*, detail: Any, base: Any) -> list[Any] | None:
|
|
12695
|
+
base_tag_items = base.get("tagItems") if isinstance(base, dict) and isinstance(base.get("tagItems"), list) else None
|
|
12696
|
+
detail_tag_items = detail.get("tagItems") if isinstance(detail, dict) and isinstance(detail.get("tagItems"), list) else None
|
|
12697
|
+
if _package_tag_items_include_groups(base_tag_items):
|
|
12698
|
+
return deepcopy(base_tag_items)
|
|
12699
|
+
if _package_tag_items_include_groups(detail_tag_items):
|
|
12700
|
+
return deepcopy(detail_tag_items)
|
|
12701
|
+
if detail_tag_items is not None:
|
|
12702
|
+
return deepcopy(detail_tag_items)
|
|
12703
|
+
if base_tag_items is not None:
|
|
12704
|
+
return deepcopy(base_tag_items)
|
|
12705
|
+
return None
|
|
12706
|
+
|
|
12707
|
+
|
|
12708
|
+
def _package_tag_items_include_groups(tag_items: Any) -> bool:
|
|
12709
|
+
if not isinstance(tag_items, list):
|
|
12710
|
+
return False
|
|
12711
|
+
return any(isinstance(item, dict) and _coerce_positive_int(item.get("itemType")) == 3 for item in tag_items)
|
|
12712
|
+
|
|
12713
|
+
|
|
12383
12714
|
def _flatten_package_resource_identities(items: Any, *, public: bool) -> set[tuple[str, str]]:
|
|
12384
12715
|
flattened: set[tuple[str, str]] = set()
|
|
12385
12716
|
|
|
@@ -12690,119 +13021,1058 @@ def _verify_package_attachment(packages: PackageTools, *, profile: str, tag_id:
|
|
|
12690
13021
|
return last_result
|
|
12691
13022
|
|
|
12692
13023
|
|
|
12693
|
-
def
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
|
|
12701
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
if
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12757
|
-
|
|
12758
|
-
|
|
12759
|
-
|
|
12760
|
-
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
12801
|
-
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
13024
|
+
def _field_question_overlay_keys(field: dict[str, Any]) -> set[str]:
|
|
13025
|
+
raw_value = field.get("_question_overlay_keys")
|
|
13026
|
+
if isinstance(raw_value, set):
|
|
13027
|
+
return {str(item) for item in raw_value if isinstance(item, str) and item}
|
|
13028
|
+
if isinstance(raw_value, list):
|
|
13029
|
+
return {str(item) for item in raw_value if isinstance(item, str) and item}
|
|
13030
|
+
return set()
|
|
13031
|
+
|
|
13032
|
+
|
|
13033
|
+
def _field_needs_question_rebuild(field: dict[str, Any]) -> bool:
|
|
13034
|
+
return not isinstance(field.get("_question_template"), dict) or bool(field.get("_question_rebuild_required"))
|
|
13035
|
+
|
|
13036
|
+
|
|
13037
|
+
def _extract_template_row_lengths(schema: dict[str, Any]) -> tuple[dict[int, int], dict[str, int]]:
|
|
13038
|
+
lengths_by_que_id: dict[int, int] = {}
|
|
13039
|
+
lengths_by_title: dict[str, int] = {}
|
|
13040
|
+
|
|
13041
|
+
def remember_row(row: Any) -> None:
|
|
13042
|
+
if not isinstance(row, list):
|
|
13043
|
+
return
|
|
13044
|
+
questions = [question for question in row if isinstance(question, dict)]
|
|
13045
|
+
row_length = len(questions)
|
|
13046
|
+
if row_length <= 0:
|
|
13047
|
+
return
|
|
13048
|
+
for question in questions:
|
|
13049
|
+
que_id = _coerce_nonnegative_int(question.get("queId"))
|
|
13050
|
+
if que_id is not None:
|
|
13051
|
+
lengths_by_que_id[que_id] = row_length
|
|
13052
|
+
title = str(question.get("queTitle") or "").strip()
|
|
13053
|
+
if title:
|
|
13054
|
+
lengths_by_title[title] = row_length
|
|
13055
|
+
|
|
13056
|
+
for row in schema.get("formQues", []) or []:
|
|
13057
|
+
if not isinstance(row, list):
|
|
13058
|
+
continue
|
|
13059
|
+
if len(row) == 1 and isinstance(row[0], dict) and _coerce_positive_int(row[0].get("queType")) == 24:
|
|
13060
|
+
for inner_row in row[0].get("innerQuestions", []) or []:
|
|
13061
|
+
remember_row(inner_row)
|
|
13062
|
+
continue
|
|
13063
|
+
remember_row(row)
|
|
13064
|
+
return lengths_by_que_id, lengths_by_title
|
|
13065
|
+
|
|
13066
|
+
|
|
13067
|
+
def _field_template_row_length(
|
|
13068
|
+
field: dict[str, Any],
|
|
13069
|
+
*,
|
|
13070
|
+
lengths_by_que_id: dict[int, int],
|
|
13071
|
+
lengths_by_title: dict[str, int],
|
|
13072
|
+
) -> int | None:
|
|
13073
|
+
que_id = _coerce_nonnegative_int(field.get("que_id"))
|
|
13074
|
+
if que_id is not None and que_id in lengths_by_que_id:
|
|
13075
|
+
return lengths_by_que_id[que_id]
|
|
13076
|
+
template = field.get("_question_template")
|
|
13077
|
+
if isinstance(template, dict):
|
|
13078
|
+
template_que_id = _coerce_nonnegative_int(template.get("queId"))
|
|
13079
|
+
if template_que_id is not None and template_que_id in lengths_by_que_id:
|
|
13080
|
+
return lengths_by_que_id[template_que_id]
|
|
13081
|
+
template_title = str(template.get("queTitle") or "").strip()
|
|
13082
|
+
if template_title and template_title in lengths_by_title:
|
|
13083
|
+
return lengths_by_title[template_title]
|
|
13084
|
+
field_name = str(field.get("name") or "").strip()
|
|
13085
|
+
if field_name and field_name in lengths_by_title:
|
|
13086
|
+
return lengths_by_title[field_name]
|
|
13087
|
+
return None
|
|
13088
|
+
|
|
13089
|
+
|
|
13090
|
+
def _row_needs_width_reflow(expected_template_lengths: list[int], current_row_length: int) -> bool:
|
|
13091
|
+
if current_row_length <= 0:
|
|
13092
|
+
return False
|
|
13093
|
+
return any(length != current_row_length for length in expected_template_lengths)
|
|
13094
|
+
|
|
13095
|
+
|
|
13096
|
+
_FORM_SAVE_BASE_KEYS = (
|
|
13097
|
+
"formDesc",
|
|
13098
|
+
"formTheme",
|
|
13099
|
+
"formAttach",
|
|
13100
|
+
"formStyle",
|
|
13101
|
+
"serialNumType",
|
|
13102
|
+
"serialNumConfig",
|
|
13103
|
+
"attachVisibleOnlyConfig",
|
|
13104
|
+
"externalLang",
|
|
13105
|
+
"hideCopyright",
|
|
13106
|
+
)
|
|
13107
|
+
|
|
13108
|
+
_QUESTION_RELATION_SAVE_KEYS = (
|
|
13109
|
+
"queId",
|
|
13110
|
+
"relationType",
|
|
13111
|
+
"displayedQueId",
|
|
13112
|
+
"qlinkerAlias",
|
|
13113
|
+
"displayedQueInfo",
|
|
13114
|
+
"aliasConfig",
|
|
13115
|
+
"matchRules",
|
|
13116
|
+
"tableMatchRules",
|
|
13117
|
+
"matchRuleType",
|
|
13118
|
+
"matchRuleFormula",
|
|
13119
|
+
"sortConfig",
|
|
13120
|
+
)
|
|
13121
|
+
|
|
13122
|
+
_RELATION_QUESTION_SAVE_KEYS = (
|
|
13123
|
+
"queId",
|
|
13124
|
+
"queTempId",
|
|
13125
|
+
"queType",
|
|
13126
|
+
"queOriginType",
|
|
13127
|
+
"queTitle",
|
|
13128
|
+
"queWidth",
|
|
13129
|
+
"scanType",
|
|
13130
|
+
"status",
|
|
13131
|
+
"required",
|
|
13132
|
+
"queHint",
|
|
13133
|
+
"linkedQuestions",
|
|
13134
|
+
"logicalShow",
|
|
13135
|
+
"queDefaultType",
|
|
13136
|
+
"queDefaultValue",
|
|
13137
|
+
"queDefaultValues",
|
|
13138
|
+
"subQueWidth",
|
|
13139
|
+
"innerQuestions",
|
|
13140
|
+
"minOpts",
|
|
13141
|
+
"maxOpts",
|
|
13142
|
+
"beingHide",
|
|
13143
|
+
"beingDesensitized",
|
|
13144
|
+
"relationDisplayMode",
|
|
13145
|
+
"customRenderConfig",
|
|
13146
|
+
)
|
|
13147
|
+
|
|
13148
|
+
_REFERENCE_CONFIG_SAVE_KEYS = (
|
|
13149
|
+
"referAppKey",
|
|
13150
|
+
"referQueId",
|
|
13151
|
+
"customButtonText",
|
|
13152
|
+
"beingTableSource",
|
|
13153
|
+
"referMatchRules",
|
|
13154
|
+
"canAddData",
|
|
13155
|
+
"dataAdditionButtonText",
|
|
13156
|
+
"canViewProcessLog",
|
|
13157
|
+
"optionalDataNum",
|
|
13158
|
+
"beingDataLogVisible",
|
|
13159
|
+
"beingDefaultFormulaAutoFillEnabled",
|
|
13160
|
+
"defaultValueMatchRules",
|
|
13161
|
+
"configShowForm",
|
|
13162
|
+
"configSortFieldId",
|
|
13163
|
+
"configAsc",
|
|
13164
|
+
"dataShowForm",
|
|
13165
|
+
"defaultRow",
|
|
13166
|
+
"fieldNameShow",
|
|
13167
|
+
"dataSortFieldId",
|
|
13168
|
+
"dataSortAsc",
|
|
13169
|
+
)
|
|
13170
|
+
|
|
13171
|
+
_REFERENCE_QUESTION_SAVE_KEYS = (
|
|
13172
|
+
"queId",
|
|
13173
|
+
"queTitle",
|
|
13174
|
+
"queType",
|
|
13175
|
+
"queAuth",
|
|
13176
|
+
"ordinal",
|
|
13177
|
+
)
|
|
13178
|
+
|
|
13179
|
+
_REFERENCE_FILL_RULE_SAVE_KEYS = (
|
|
13180
|
+
"queId",
|
|
13181
|
+
"relatedQueId",
|
|
13182
|
+
"queTitle",
|
|
13183
|
+
"relatedQueTitle",
|
|
13184
|
+
)
|
|
13185
|
+
|
|
13186
|
+
_REFERENCE_AUTH_QUESTION_SAVE_KEYS = (
|
|
13187
|
+
"queId",
|
|
13188
|
+
"queAuth",
|
|
13189
|
+
)
|
|
13190
|
+
|
|
13191
|
+
|
|
13192
|
+
def _copy_present_keys(
|
|
13193
|
+
source: dict[str, Any],
|
|
13194
|
+
keys: tuple[str, ...],
|
|
13195
|
+
*,
|
|
13196
|
+
keep_none_keys: tuple[str, ...] = (),
|
|
13197
|
+
) -> dict[str, Any]:
|
|
13198
|
+
payload: dict[str, Any] = {}
|
|
13199
|
+
keep_none = set(keep_none_keys)
|
|
13200
|
+
for key in keys:
|
|
13201
|
+
if key not in source:
|
|
13202
|
+
continue
|
|
13203
|
+
value = source.get(key)
|
|
13204
|
+
if value is None and key not in keep_none:
|
|
13205
|
+
continue
|
|
13206
|
+
payload[key] = deepcopy(value)
|
|
13207
|
+
return payload
|
|
13208
|
+
|
|
13209
|
+
|
|
13210
|
+
def _looks_like_backend_encoded_formula(value: str) -> bool:
|
|
13211
|
+
if len(value) <= 32:
|
|
13212
|
+
return False
|
|
13213
|
+
encoded = value[16:-16]
|
|
13214
|
+
if not encoded:
|
|
13215
|
+
return False
|
|
13216
|
+
try:
|
|
13217
|
+
decoded = base64.b64decode(encoded, validate=True).decode("utf-8")
|
|
13218
|
+
unquote_plus(decoded)
|
|
13219
|
+
except Exception:
|
|
13220
|
+
return False
|
|
13221
|
+
return True
|
|
13222
|
+
|
|
13223
|
+
|
|
13224
|
+
def _encode_formula_for_backend_save(value: Any) -> Any:
|
|
13225
|
+
if not isinstance(value, str) or not value:
|
|
13226
|
+
return value
|
|
13227
|
+
if _looks_like_backend_encoded_formula(value):
|
|
13228
|
+
return value
|
|
13229
|
+
encoded = quote_plus(value, encoding="utf-8")
|
|
13230
|
+
b64_value = base64.b64encode(encoded.encode("utf-8")).decode("ascii")
|
|
13231
|
+
alphabet = string.ascii_letters + string.digits
|
|
13232
|
+
prefix = "".join(random.choice(alphabet) for _ in range(16))
|
|
13233
|
+
suffix = "".join(random.choice(alphabet) for _ in range(16))
|
|
13234
|
+
return f"{prefix}{b64_value}{suffix}"
|
|
13235
|
+
|
|
13236
|
+
|
|
13237
|
+
def _normalize_formula_defaults_for_save(value: Any) -> None:
|
|
13238
|
+
if isinstance(value, list):
|
|
13239
|
+
for item in value:
|
|
13240
|
+
_normalize_formula_defaults_for_save(item)
|
|
13241
|
+
return
|
|
13242
|
+
if not isinstance(value, dict):
|
|
13243
|
+
return
|
|
13244
|
+
if _coerce_any_int(value.get("queDefaultType")) == DEFAULT_TYPE_FORMULA and value.get("queDefaultValue"):
|
|
13245
|
+
value["queDefaultValue"] = _encode_formula_for_backend_save(value.get("queDefaultValue"))
|
|
13246
|
+
for key in ("subQuestions", "innerQuestions"):
|
|
13247
|
+
nested = value.get(key)
|
|
13248
|
+
if isinstance(nested, (list, dict)):
|
|
13249
|
+
_normalize_formula_defaults_for_save(nested)
|
|
13250
|
+
|
|
13251
|
+
|
|
13252
|
+
def _normalize_reference_question_for_save(value: Any, *, ordinal: int) -> dict[str, Any] | None:
|
|
13253
|
+
if not isinstance(value, dict):
|
|
13254
|
+
return None
|
|
13255
|
+
payload = _copy_present_keys(value, _REFERENCE_QUESTION_SAVE_KEYS)
|
|
13256
|
+
que_id = _coerce_any_int(value.get("queId"))
|
|
13257
|
+
if que_id is not None:
|
|
13258
|
+
payload["queId"] = que_id
|
|
13259
|
+
if "ordinal" not in payload:
|
|
13260
|
+
payload["ordinal"] = _coerce_nonnegative_int(value.get("ordinal"))
|
|
13261
|
+
if payload.get("ordinal") is None:
|
|
13262
|
+
payload["ordinal"] = ordinal
|
|
13263
|
+
if not any(key in payload for key in ("queId", "queTitle", "queType")):
|
|
13264
|
+
return None
|
|
13265
|
+
return payload
|
|
13266
|
+
|
|
13267
|
+
|
|
13268
|
+
def _normalize_reference_fill_rule_for_save(value: Any) -> dict[str, Any] | None:
|
|
13269
|
+
if not isinstance(value, dict):
|
|
13270
|
+
return None
|
|
13271
|
+
payload = _copy_present_keys(value, _REFERENCE_FILL_RULE_SAVE_KEYS)
|
|
13272
|
+
que_id = _coerce_nonnegative_int(value.get("queId"))
|
|
13273
|
+
related_que_id = _coerce_nonnegative_int(value.get("relatedQueId", value.get("referQueId")))
|
|
13274
|
+
if que_id is not None:
|
|
13275
|
+
payload["queId"] = que_id
|
|
13276
|
+
if related_que_id is not None:
|
|
13277
|
+
payload["relatedQueId"] = related_que_id
|
|
13278
|
+
if "relatedQueTitle" not in payload and value.get("referQueTitle") is not None:
|
|
13279
|
+
payload["relatedQueTitle"] = str(value.get("referQueTitle") or "")
|
|
13280
|
+
if "queId" not in payload or "relatedQueId" not in payload:
|
|
13281
|
+
return None
|
|
13282
|
+
return payload
|
|
13283
|
+
|
|
13284
|
+
|
|
13285
|
+
def _normalize_reference_auth_question_for_save(value: Any) -> dict[str, Any] | None:
|
|
13286
|
+
if not isinstance(value, dict):
|
|
13287
|
+
return None
|
|
13288
|
+
payload = _copy_present_keys(value, _REFERENCE_AUTH_QUESTION_SAVE_KEYS)
|
|
13289
|
+
que_id = _coerce_any_int(value.get("queId"))
|
|
13290
|
+
if que_id is not None:
|
|
13291
|
+
payload["queId"] = que_id
|
|
13292
|
+
que_auth = _coerce_nonnegative_int(value.get("queAuth"))
|
|
13293
|
+
if que_auth is not None:
|
|
13294
|
+
payload["queAuth"] = que_auth
|
|
13295
|
+
sub_ques = [
|
|
13296
|
+
item
|
|
13297
|
+
for item in (
|
|
13298
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13299
|
+
for raw_item in cast(list[Any], value.get("subQues") or [])
|
|
13300
|
+
)
|
|
13301
|
+
if item is not None
|
|
13302
|
+
]
|
|
13303
|
+
inner_ques = [
|
|
13304
|
+
item
|
|
13305
|
+
for item in (
|
|
13306
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13307
|
+
for raw_item in cast(list[Any], value.get("innerQues") or [])
|
|
13308
|
+
)
|
|
13309
|
+
if item is not None
|
|
13310
|
+
]
|
|
13311
|
+
if sub_ques or "subQues" in value:
|
|
13312
|
+
payload["subQues"] = sub_ques
|
|
13313
|
+
if inner_ques or "innerQues" in value:
|
|
13314
|
+
payload["innerQues"] = inner_ques
|
|
13315
|
+
if "queId" not in payload or "queAuth" not in payload:
|
|
13316
|
+
return None
|
|
13317
|
+
return payload
|
|
13318
|
+
|
|
13319
|
+
|
|
13320
|
+
def _dedupe_reference_auth_questions(auth_questions: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
13321
|
+
deduped: list[dict[str, Any]] = []
|
|
13322
|
+
seen_que_ids: set[int] = set()
|
|
13323
|
+
for item in auth_questions:
|
|
13324
|
+
normalized_item = _normalize_reference_auth_question_for_save(item)
|
|
13325
|
+
if normalized_item is None:
|
|
13326
|
+
continue
|
|
13327
|
+
que_id = _coerce_any_int(normalized_item.get("queId"))
|
|
13328
|
+
if que_id is None or que_id in seen_que_ids:
|
|
13329
|
+
continue
|
|
13330
|
+
seen_que_ids.add(que_id)
|
|
13331
|
+
deduped.append(normalized_item)
|
|
13332
|
+
return deduped
|
|
13333
|
+
|
|
13334
|
+
|
|
13335
|
+
_REFERENCE_FIELD_HIDDEN_AUTH = 2
|
|
13336
|
+
_REFERENCE_FIELD_VISIBLE_AUTH = 3
|
|
13337
|
+
|
|
13338
|
+
|
|
13339
|
+
def _synthesize_reference_auth_questions_for_save(
|
|
13340
|
+
*,
|
|
13341
|
+
source: dict[str, Any],
|
|
13342
|
+
field: dict[str, Any],
|
|
13343
|
+
) -> list[dict[str, Any]]:
|
|
13344
|
+
config = field.get("config") if isinstance(field.get("config"), dict) else {}
|
|
13345
|
+
synthesized: list[dict[str, Any]] = []
|
|
13346
|
+
|
|
13347
|
+
if isinstance(config.get("refer_auth_ques"), list):
|
|
13348
|
+
synthesized.extend(cast(list[dict[str, Any]], config.get("refer_auth_ques") or []))
|
|
13349
|
+
if synthesized:
|
|
13350
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13351
|
+
|
|
13352
|
+
refer_question_ids_by_name: dict[str, int] = {}
|
|
13353
|
+
for raw_item in cast(list[Any], source.get("referQuestions") or []):
|
|
13354
|
+
if not isinstance(raw_item, dict):
|
|
13355
|
+
continue
|
|
13356
|
+
que_id = _coerce_any_int(raw_item.get("queId"))
|
|
13357
|
+
name = str(raw_item.get("queTitle") or "").strip()
|
|
13358
|
+
if que_id is None or not name or name in refer_question_ids_by_name:
|
|
13359
|
+
continue
|
|
13360
|
+
refer_question_ids_by_name[name] = que_id
|
|
13361
|
+
|
|
13362
|
+
visible_fields = cast(list[dict[str, Any]], field.get("visible_fields") or [])
|
|
13363
|
+
for item in visible_fields:
|
|
13364
|
+
if not isinstance(item, dict):
|
|
13365
|
+
continue
|
|
13366
|
+
que_id = _coerce_any_int(item.get("que_id"))
|
|
13367
|
+
if que_id is None:
|
|
13368
|
+
name = str(item.get("name") or "").strip()
|
|
13369
|
+
que_id = refer_question_ids_by_name.get(name)
|
|
13370
|
+
if que_id is None:
|
|
13371
|
+
continue
|
|
13372
|
+
synthesized.append({"queId": que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
|
|
13373
|
+
if synthesized:
|
|
13374
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13375
|
+
|
|
13376
|
+
auth_field_que_ids = cast(list[Any], config.get("auth_field_que_ids") or [])
|
|
13377
|
+
for raw_que_id in auth_field_que_ids:
|
|
13378
|
+
que_id = _coerce_any_int(raw_que_id)
|
|
13379
|
+
if que_id is None:
|
|
13380
|
+
continue
|
|
13381
|
+
synthesized.append({"queId": que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
|
|
13382
|
+
if synthesized:
|
|
13383
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13384
|
+
|
|
13385
|
+
for raw_item in cast(list[Any], source.get("referQuestions") or []):
|
|
13386
|
+
if not isinstance(raw_item, dict):
|
|
13387
|
+
continue
|
|
13388
|
+
que_id = _coerce_any_int(raw_item.get("queId"))
|
|
13389
|
+
if que_id is None:
|
|
13390
|
+
continue
|
|
13391
|
+
synthesized.append(
|
|
13392
|
+
{
|
|
13393
|
+
"queId": que_id,
|
|
13394
|
+
"queAuth": _REFERENCE_FIELD_VISIBLE_AUTH,
|
|
13395
|
+
}
|
|
13396
|
+
)
|
|
13397
|
+
if synthesized:
|
|
13398
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13399
|
+
|
|
13400
|
+
fallback_que_id = _coerce_any_int(field.get("target_field_que_id"))
|
|
13401
|
+
if fallback_que_id is not None:
|
|
13402
|
+
synthesized.append({"queId": fallback_que_id, "queAuth": _REFERENCE_FIELD_VISIBLE_AUTH})
|
|
13403
|
+
return _dedupe_reference_auth_questions(synthesized)
|
|
13404
|
+
|
|
13405
|
+
|
|
13406
|
+
def _reference_question_auth_overrides_for_save(
|
|
13407
|
+
*,
|
|
13408
|
+
source: dict[str, Any],
|
|
13409
|
+
field: dict[str, Any],
|
|
13410
|
+
) -> dict[int, int]:
|
|
13411
|
+
overrides: dict[int, int] = {}
|
|
13412
|
+
visible_que_ids: set[int] = set()
|
|
13413
|
+
|
|
13414
|
+
for item in cast(list[Any], field.get("visible_fields") or []):
|
|
13415
|
+
if not isinstance(item, dict):
|
|
13416
|
+
continue
|
|
13417
|
+
que_id = _coerce_any_int(item.get("que_id"))
|
|
13418
|
+
if que_id is not None:
|
|
13419
|
+
visible_que_ids.add(que_id)
|
|
13420
|
+
|
|
13421
|
+
if not visible_que_ids:
|
|
13422
|
+
refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
|
|
13423
|
+
for item in refer_auth_ques:
|
|
13424
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13425
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13426
|
+
if que_id is None or que_auth is None:
|
|
13427
|
+
continue
|
|
13428
|
+
overrides[que_id] = que_auth
|
|
13429
|
+
if overrides:
|
|
13430
|
+
return overrides
|
|
13431
|
+
|
|
13432
|
+
for raw_item in cast(list[Any], source.get("referQuestions") or []):
|
|
13433
|
+
if not isinstance(raw_item, dict):
|
|
13434
|
+
continue
|
|
13435
|
+
que_id = _coerce_any_int(raw_item.get("queId"))
|
|
13436
|
+
if que_id is None:
|
|
13437
|
+
continue
|
|
13438
|
+
overrides[que_id] = (
|
|
13439
|
+
_REFERENCE_FIELD_VISIBLE_AUTH if que_id in visible_que_ids else _REFERENCE_FIELD_HIDDEN_AUTH
|
|
13440
|
+
)
|
|
13441
|
+
return overrides
|
|
13442
|
+
|
|
13443
|
+
|
|
13444
|
+
def _reference_question_matches_visible_selector(question: dict[str, Any], selector: dict[str, Any]) -> bool:
|
|
13445
|
+
question_que_id = _coerce_any_int(question.get("queId"))
|
|
13446
|
+
selector_que_id = _coerce_any_int(selector.get("que_id"))
|
|
13447
|
+
if question_que_id is not None and selector_que_id is not None and question_que_id == selector_que_id:
|
|
13448
|
+
return True
|
|
13449
|
+
question_name = str(question.get("queTitle") or "").strip()
|
|
13450
|
+
selector_name = str(selector.get("name") or "").strip()
|
|
13451
|
+
return bool(question_name and selector_name and question_name == selector_name)
|
|
13452
|
+
|
|
13453
|
+
|
|
13454
|
+
def _build_reference_question_from_visible_selector(
|
|
13455
|
+
selector: dict[str, Any],
|
|
13456
|
+
*,
|
|
13457
|
+
ordinal: int,
|
|
13458
|
+
) -> dict[str, Any] | None:
|
|
13459
|
+
return _normalize_reference_question_for_save(
|
|
13460
|
+
{
|
|
13461
|
+
"queId": _coerce_any_int(selector.get("que_id")),
|
|
13462
|
+
"queTitle": str(selector.get("name") or "").strip() or None,
|
|
13463
|
+
"queType": str(selector.get("type") or "2"),
|
|
13464
|
+
"ordinal": ordinal,
|
|
13465
|
+
},
|
|
13466
|
+
ordinal=ordinal,
|
|
13467
|
+
)
|
|
13468
|
+
|
|
13469
|
+
|
|
13470
|
+
def _canonicalize_reference_questions_for_save(
|
|
13471
|
+
*,
|
|
13472
|
+
source: dict[str, Any],
|
|
13473
|
+
field: dict[str, Any],
|
|
13474
|
+
) -> list[dict[str, Any]]:
|
|
13475
|
+
relation_config_explicit = bool(field.get("_relation_config_explicit"))
|
|
13476
|
+
normalized_source_questions = [
|
|
13477
|
+
item
|
|
13478
|
+
for item in (
|
|
13479
|
+
_normalize_reference_question_for_save(raw_item, ordinal=index)
|
|
13480
|
+
for index, raw_item in enumerate(cast(list[Any], source.get("referQuestions") or []), start=1)
|
|
13481
|
+
)
|
|
13482
|
+
if item is not None
|
|
13483
|
+
]
|
|
13484
|
+
if not relation_config_explicit:
|
|
13485
|
+
return normalized_source_questions
|
|
13486
|
+
|
|
13487
|
+
display_field = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
|
|
13488
|
+
visible_fields = [item for item in cast(list[Any], field.get("visible_fields") or []) if isinstance(item, dict)]
|
|
13489
|
+
ordered_visible_selectors: list[dict[str, Any]] = []
|
|
13490
|
+
if display_field is not None:
|
|
13491
|
+
ordered_visible_selectors.append(display_field)
|
|
13492
|
+
for item in visible_fields:
|
|
13493
|
+
if any(_relation_target_field_matches(existing, item) for existing in ordered_visible_selectors):
|
|
13494
|
+
continue
|
|
13495
|
+
ordered_visible_selectors.append(item)
|
|
13496
|
+
|
|
13497
|
+
if not ordered_visible_selectors:
|
|
13498
|
+
return normalized_source_questions
|
|
13499
|
+
|
|
13500
|
+
canonical_questions: list[dict[str, Any]] = []
|
|
13501
|
+
used_source_indexes: set[int] = set()
|
|
13502
|
+
|
|
13503
|
+
for ordinal, selector in enumerate(ordered_visible_selectors, start=1):
|
|
13504
|
+
matched_index: int | None = None
|
|
13505
|
+
matched_item: dict[str, Any] | None = None
|
|
13506
|
+
for index, item in enumerate(normalized_source_questions):
|
|
13507
|
+
if index in used_source_indexes:
|
|
13508
|
+
continue
|
|
13509
|
+
if _reference_question_matches_visible_selector(item, selector):
|
|
13510
|
+
matched_index = index
|
|
13511
|
+
matched_item = deepcopy(item)
|
|
13512
|
+
break
|
|
13513
|
+
if matched_item is None:
|
|
13514
|
+
matched_item = _build_reference_question_from_visible_selector(selector, ordinal=ordinal)
|
|
13515
|
+
if matched_item is None:
|
|
13516
|
+
continue
|
|
13517
|
+
matched_item["ordinal"] = ordinal
|
|
13518
|
+
canonical_questions.append(matched_item)
|
|
13519
|
+
if matched_index is not None:
|
|
13520
|
+
used_source_indexes.add(matched_index)
|
|
13521
|
+
|
|
13522
|
+
source_target_app_key = str(source.get("referAppKey") or "").strip()
|
|
13523
|
+
target_app_key = str(field.get("target_app_key") or "").strip()
|
|
13524
|
+
preserve_remaining_source_questions = not source_target_app_key or source_target_app_key == target_app_key
|
|
13525
|
+
|
|
13526
|
+
if preserve_remaining_source_questions:
|
|
13527
|
+
next_ordinal = len(canonical_questions) + 1
|
|
13528
|
+
for index, item in enumerate(normalized_source_questions):
|
|
13529
|
+
if index in used_source_indexes:
|
|
13530
|
+
continue
|
|
13531
|
+
remaining_item = deepcopy(item)
|
|
13532
|
+
remaining_item["ordinal"] = next_ordinal
|
|
13533
|
+
next_ordinal += 1
|
|
13534
|
+
canonical_questions.append(remaining_item)
|
|
13535
|
+
|
|
13536
|
+
return canonical_questions
|
|
13537
|
+
|
|
13538
|
+
|
|
13539
|
+
def _canonicalize_reference_auth_questions_for_save(
|
|
13540
|
+
*,
|
|
13541
|
+
source: dict[str, Any],
|
|
13542
|
+
refer_questions: list[dict[str, Any]],
|
|
13543
|
+
relation_config_explicit: bool,
|
|
13544
|
+
) -> list[dict[str, Any]]:
|
|
13545
|
+
source_auth_questions = [
|
|
13546
|
+
item
|
|
13547
|
+
for item in (
|
|
13548
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13549
|
+
for raw_item in cast(list[Any], source.get("referAuthQues") or [])
|
|
13550
|
+
)
|
|
13551
|
+
if item is not None
|
|
13552
|
+
]
|
|
13553
|
+
source_auth_by_que_id: dict[int, dict[str, Any]] = {}
|
|
13554
|
+
for item in source_auth_questions:
|
|
13555
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13556
|
+
if que_id is None or que_id in source_auth_by_que_id:
|
|
13557
|
+
continue
|
|
13558
|
+
source_auth_by_que_id[que_id] = item
|
|
13559
|
+
|
|
13560
|
+
if not relation_config_explicit:
|
|
13561
|
+
auth_questions: list[dict[str, Any]] = []
|
|
13562
|
+
seen_que_ids: set[int] = set()
|
|
13563
|
+
refer_question_auth_by_que_id: dict[int, int] = {}
|
|
13564
|
+
for item in refer_questions:
|
|
13565
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13566
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13567
|
+
if que_id is None or que_auth is None or que_id in refer_question_auth_by_que_id:
|
|
13568
|
+
continue
|
|
13569
|
+
refer_question_auth_by_que_id[que_id] = que_auth
|
|
13570
|
+
|
|
13571
|
+
for item in source_auth_questions:
|
|
13572
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13573
|
+
if que_id is None or que_id in seen_que_ids:
|
|
13574
|
+
continue
|
|
13575
|
+
payload = deepcopy(item)
|
|
13576
|
+
if que_id in refer_question_auth_by_que_id:
|
|
13577
|
+
payload["queAuth"] = refer_question_auth_by_que_id[que_id]
|
|
13578
|
+
auth_questions.append(payload)
|
|
13579
|
+
seen_que_ids.add(que_id)
|
|
13580
|
+
|
|
13581
|
+
for item in refer_questions:
|
|
13582
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13583
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13584
|
+
if que_id is None or que_auth is None or que_id in seen_que_ids:
|
|
13585
|
+
continue
|
|
13586
|
+
payload = deepcopy(source_auth_by_que_id.get(que_id) or {"queId": que_id})
|
|
13587
|
+
payload["queId"] = que_id
|
|
13588
|
+
payload["queAuth"] = que_auth
|
|
13589
|
+
auth_questions.append(payload)
|
|
13590
|
+
seen_que_ids.add(que_id)
|
|
13591
|
+
|
|
13592
|
+
return _dedupe_reference_auth_questions(auth_questions)
|
|
13593
|
+
|
|
13594
|
+
auth_questions: list[dict[str, Any]] = []
|
|
13595
|
+
for item in refer_questions:
|
|
13596
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13597
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13598
|
+
if que_id is None or que_auth is None:
|
|
13599
|
+
continue
|
|
13600
|
+
payload = deepcopy(source_auth_by_que_id.get(que_id) or {"queId": que_id})
|
|
13601
|
+
payload["queId"] = que_id
|
|
13602
|
+
payload["queAuth"] = que_auth
|
|
13603
|
+
auth_questions.append(payload)
|
|
13604
|
+
return _dedupe_reference_auth_questions(auth_questions)
|
|
13605
|
+
|
|
13606
|
+
|
|
13607
|
+
def _enforce_reference_config_consistency_for_save(
|
|
13608
|
+
payload: dict[str, Any],
|
|
13609
|
+
*,
|
|
13610
|
+
field: dict[str, Any],
|
|
13611
|
+
) -> dict[str, Any]:
|
|
13612
|
+
relation_config_explicit = bool(field.get("_relation_config_explicit"))
|
|
13613
|
+
refer_questions = [
|
|
13614
|
+
item
|
|
13615
|
+
for item in (
|
|
13616
|
+
_normalize_reference_question_for_save(raw_item, ordinal=index)
|
|
13617
|
+
for index, raw_item in enumerate(cast(list[Any], payload.get("referQuestions") or []), start=1)
|
|
13618
|
+
)
|
|
13619
|
+
if item is not None
|
|
13620
|
+
]
|
|
13621
|
+
if not refer_questions:
|
|
13622
|
+
return payload
|
|
13623
|
+
|
|
13624
|
+
refer_auth_ques = _dedupe_reference_auth_questions(
|
|
13625
|
+
[
|
|
13626
|
+
item
|
|
13627
|
+
for item in (
|
|
13628
|
+
_normalize_reference_auth_question_for_save(raw_item)
|
|
13629
|
+
for raw_item in cast(list[Any], payload.get("referAuthQues") or [])
|
|
13630
|
+
)
|
|
13631
|
+
if item is not None
|
|
13632
|
+
]
|
|
13633
|
+
)
|
|
13634
|
+
refer_auth_by_que_id: dict[int, int] = {}
|
|
13635
|
+
for item in refer_auth_ques:
|
|
13636
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13637
|
+
que_auth = _coerce_nonnegative_int(item.get("queAuth"))
|
|
13638
|
+
if que_id is None or que_auth is None or que_id in refer_auth_by_que_id:
|
|
13639
|
+
continue
|
|
13640
|
+
refer_auth_by_que_id[que_id] = que_auth
|
|
13641
|
+
|
|
13642
|
+
display_field_que_id = _coerce_any_int(payload.get("referQueId"))
|
|
13643
|
+
if display_field_que_id is None:
|
|
13644
|
+
display_field_que_id = _coerce_any_int(field.get("target_field_que_id"))
|
|
13645
|
+
if display_field_que_id is not None:
|
|
13646
|
+
payload["referQueId"] = display_field_que_id
|
|
13647
|
+
|
|
13648
|
+
if relation_config_explicit:
|
|
13649
|
+
if display_field_que_id is not None and not any(
|
|
13650
|
+
_coerce_any_int(item.get("queId")) == display_field_que_id for item in refer_questions
|
|
13651
|
+
):
|
|
13652
|
+
display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
|
|
13653
|
+
display_question = (
|
|
13654
|
+
_build_reference_question_from_visible_selector(display_selector, ordinal=1)
|
|
13655
|
+
if display_selector is not None
|
|
13656
|
+
else None
|
|
13657
|
+
)
|
|
13658
|
+
if display_question is not None:
|
|
13659
|
+
display_question["queId"] = display_field_que_id
|
|
13660
|
+
display_question["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
|
|
13661
|
+
refer_questions = [display_question, *refer_questions]
|
|
13662
|
+
|
|
13663
|
+
if display_field_que_id is not None:
|
|
13664
|
+
display_questions = [
|
|
13665
|
+
item for item in refer_questions if _coerce_any_int(item.get("queId")) == display_field_que_id
|
|
13666
|
+
]
|
|
13667
|
+
trailing_questions = [
|
|
13668
|
+
item for item in refer_questions if _coerce_any_int(item.get("queId")) != display_field_que_id
|
|
13669
|
+
]
|
|
13670
|
+
refer_questions = [*display_questions, *trailing_questions]
|
|
13671
|
+
|
|
13672
|
+
for ordinal, item in enumerate(refer_questions, start=1):
|
|
13673
|
+
que_id = _coerce_any_int(item.get("queId"))
|
|
13674
|
+
if que_id is None:
|
|
13675
|
+
continue
|
|
13676
|
+
item["ordinal"] = ordinal
|
|
13677
|
+
item["queAuth"] = refer_auth_by_que_id.get(
|
|
13678
|
+
que_id,
|
|
13679
|
+
_coerce_nonnegative_int(item.get("queAuth")) or _REFERENCE_FIELD_VISIBLE_AUTH,
|
|
13680
|
+
)
|
|
13681
|
+
if display_field_que_id is not None and que_id == display_field_que_id:
|
|
13682
|
+
item["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
|
|
13683
|
+
|
|
13684
|
+
payload["referQuestions"] = refer_questions
|
|
13685
|
+
payload["referAuthQues"] = _canonicalize_reference_auth_questions_for_save(
|
|
13686
|
+
source={"referAuthQues": refer_auth_ques},
|
|
13687
|
+
refer_questions=refer_questions,
|
|
13688
|
+
relation_config_explicit=relation_config_explicit,
|
|
13689
|
+
)
|
|
13690
|
+
return payload
|
|
13691
|
+
|
|
13692
|
+
|
|
13693
|
+
def _normalize_reference_config_for_save(
|
|
13694
|
+
reference: Any,
|
|
13695
|
+
*,
|
|
13696
|
+
field: dict[str, Any],
|
|
13697
|
+
) -> dict[str, Any]:
|
|
13698
|
+
source = reference if isinstance(reference, dict) else {}
|
|
13699
|
+
payload = _copy_present_keys(source, _REFERENCE_CONFIG_SAVE_KEYS)
|
|
13700
|
+
if str(field.get("target_app_key") or "").strip():
|
|
13701
|
+
payload["referAppKey"] = str(field.get("target_app_key") or "").strip()
|
|
13702
|
+
if field.get("target_field_que_id") is not None:
|
|
13703
|
+
payload["referQueId"] = _coerce_nonnegative_int(field.get("target_field_que_id"))
|
|
13704
|
+
if field.get("field_name_show") is not None:
|
|
13705
|
+
payload["fieldNameShow"] = bool(field.get("field_name_show"))
|
|
13706
|
+
|
|
13707
|
+
refer_question_auth_overrides = _reference_question_auth_overrides_for_save(source=source, field=field)
|
|
13708
|
+
refer_questions = _canonicalize_reference_questions_for_save(source=source, field=field)
|
|
13709
|
+
for index, normalized_item in enumerate(refer_questions, start=1):
|
|
13710
|
+
que_id = _coerce_any_int(normalized_item.get("queId"))
|
|
13711
|
+
if que_id is not None and que_id in refer_question_auth_overrides:
|
|
13712
|
+
normalized_item["queAuth"] = refer_question_auth_overrides[que_id]
|
|
13713
|
+
normalized_item["ordinal"] = index
|
|
13714
|
+
if refer_questions or "referQuestions" in source:
|
|
13715
|
+
payload["referQuestions"] = refer_questions
|
|
13716
|
+
|
|
13717
|
+
refer_fill_rules = [
|
|
13718
|
+
item
|
|
13719
|
+
for item in (
|
|
13720
|
+
_normalize_reference_fill_rule_for_save(raw_item)
|
|
13721
|
+
for raw_item in cast(list[Any], source.get("referFillRules") or [])
|
|
13722
|
+
)
|
|
13723
|
+
if item is not None
|
|
13724
|
+
]
|
|
13725
|
+
if refer_fill_rules or "referFillRules" in source:
|
|
13726
|
+
payload["referFillRules"] = refer_fill_rules
|
|
13727
|
+
|
|
13728
|
+
refer_auth_ques = _canonicalize_reference_auth_questions_for_save(
|
|
13729
|
+
source=source,
|
|
13730
|
+
refer_questions=refer_questions,
|
|
13731
|
+
relation_config_explicit=bool(field.get("_relation_config_explicit")),
|
|
13732
|
+
)
|
|
13733
|
+
if not refer_auth_ques:
|
|
13734
|
+
refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
|
|
13735
|
+
if refer_auth_ques or "referAuthQues" in source:
|
|
13736
|
+
payload["referAuthQues"] = refer_auth_ques
|
|
13737
|
+
|
|
13738
|
+
return _enforce_reference_config_consistency_for_save(payload, field=field)
|
|
13739
|
+
|
|
13740
|
+
|
|
13741
|
+
def _normalize_relation_question_for_save(question: dict[str, Any], *, field: dict[str, Any]) -> dict[str, Any]:
|
|
13742
|
+
payload = _copy_present_keys(question, _RELATION_QUESTION_SAVE_KEYS)
|
|
13743
|
+
overlay_keys = _field_question_overlay_keys(field)
|
|
13744
|
+
que_id = _coerce_nonnegative_int(question.get("queId"))
|
|
13745
|
+
if que_id is not None:
|
|
13746
|
+
payload["queId"] = que_id
|
|
13747
|
+
que_temp_id = _coerce_nonnegative_int(question.get("queTempId"))
|
|
13748
|
+
if que_temp_id is not None and "queId" not in payload:
|
|
13749
|
+
payload["queTempId"] = que_temp_id
|
|
13750
|
+
payload["queType"] = _coerce_positive_int(question.get("queType")) or 25
|
|
13751
|
+
payload["queTitle"] = str(field.get("name") or question.get("queTitle") or "")
|
|
13752
|
+
if "required" in overlay_keys or "required" in question or field.get("required") is not None:
|
|
13753
|
+
payload["required"] = bool(field.get("required", question.get("required", False)))
|
|
13754
|
+
if "description" in overlay_keys:
|
|
13755
|
+
payload["queHint"] = "" if field.get("description") is None else str(field.get("description"))
|
|
13756
|
+
elif "queHint" in question and question.get("queHint") is not None:
|
|
13757
|
+
payload["queHint"] = str(question.get("queHint") or "")
|
|
13758
|
+
if field.get("default_type") is not None:
|
|
13759
|
+
payload["queDefaultType"] = _coerce_positive_int(field.get("default_type")) or 1
|
|
13760
|
+
if "default_value" in field:
|
|
13761
|
+
payload["queDefaultValue"] = field.get("default_value")
|
|
13762
|
+
payload["referenceConfig"] = _normalize_reference_config_for_save(question.get("referenceConfig"), field=field)
|
|
13763
|
+
return payload
|
|
13764
|
+
|
|
13765
|
+
|
|
13766
|
+
def _build_form_save_base_payload(current_schema: dict[str, Any], title: str) -> dict[str, Any]:
|
|
13767
|
+
payload: dict[str, Any] = {"formTitle": title}
|
|
13768
|
+
for key in _FORM_SAVE_BASE_KEYS:
|
|
13769
|
+
if key in current_schema:
|
|
13770
|
+
payload[key] = deepcopy(current_schema.get(key))
|
|
13771
|
+
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
13772
|
+
payload["formQues"] = []
|
|
13773
|
+
payload["questionRelations"] = []
|
|
13774
|
+
return payload
|
|
13775
|
+
|
|
13776
|
+
|
|
13777
|
+
def _normalize_question_relations_for_save(question_relations: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
|
|
13778
|
+
normalized: list[dict[str, Any]] = []
|
|
13779
|
+
for relation in question_relations or []:
|
|
13780
|
+
if not isinstance(relation, dict):
|
|
13781
|
+
continue
|
|
13782
|
+
item: dict[str, Any] = {}
|
|
13783
|
+
for key in _QUESTION_RELATION_SAVE_KEYS:
|
|
13784
|
+
if key not in relation:
|
|
13785
|
+
continue
|
|
13786
|
+
value = relation.get(key)
|
|
13787
|
+
if value is None:
|
|
13788
|
+
continue
|
|
13789
|
+
if key == "matchRuleFormula":
|
|
13790
|
+
value = _encode_formula_for_backend_save(value)
|
|
13791
|
+
item[key] = deepcopy(value)
|
|
13792
|
+
if item:
|
|
13793
|
+
normalized.append(item)
|
|
13794
|
+
return normalized
|
|
13795
|
+
|
|
13796
|
+
|
|
13797
|
+
def _field_rename_maps(fields: list[dict[str, Any]]) -> tuple[dict[int, str], dict[str, str]]:
|
|
13798
|
+
by_que_id: dict[int, str] = {}
|
|
13799
|
+
by_title: dict[str, str] = {}
|
|
13800
|
+
|
|
13801
|
+
def visit(field: dict[str, Any]) -> None:
|
|
13802
|
+
if not isinstance(field, dict):
|
|
13803
|
+
return
|
|
13804
|
+
template = field.get("_question_template")
|
|
13805
|
+
if not isinstance(template, dict):
|
|
13806
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13807
|
+
visit(subfield)
|
|
13808
|
+
return
|
|
13809
|
+
old_title = str(template.get("queTitle") or "").strip()
|
|
13810
|
+
new_title = str(field.get("name") or "").strip()
|
|
13811
|
+
if not old_title or not new_title or old_title == new_title:
|
|
13812
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13813
|
+
visit(subfield)
|
|
13814
|
+
return
|
|
13815
|
+
que_id = _coerce_nonnegative_int(field.get("que_id"))
|
|
13816
|
+
if que_id is None:
|
|
13817
|
+
que_id = _coerce_nonnegative_int(template.get("queId"))
|
|
13818
|
+
if que_id is not None:
|
|
13819
|
+
by_que_id[que_id] = new_title
|
|
13820
|
+
by_title[old_title] = new_title
|
|
13821
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13822
|
+
visit(subfield)
|
|
13823
|
+
|
|
13824
|
+
for field in fields:
|
|
13825
|
+
visit(field)
|
|
13826
|
+
return by_que_id, by_title
|
|
13827
|
+
|
|
13828
|
+
|
|
13829
|
+
def _sync_question_title_references(value: Any, *, by_que_id: dict[int, str], by_title: dict[str, str]) -> None:
|
|
13830
|
+
if isinstance(value, list):
|
|
13831
|
+
for item in value:
|
|
13832
|
+
_sync_question_title_references(item, by_que_id=by_que_id, by_title=by_title)
|
|
13833
|
+
return
|
|
13834
|
+
if not isinstance(value, dict):
|
|
13835
|
+
return
|
|
13836
|
+
|
|
13837
|
+
title_keys = ("queTitle", "_field_id")
|
|
13838
|
+
que_id = _coerce_nonnegative_int(value.get("queId"))
|
|
13839
|
+
replacement = None
|
|
13840
|
+
if que_id is not None and que_id in by_que_id:
|
|
13841
|
+
replacement = by_que_id[que_id]
|
|
13842
|
+
elif que_id is None:
|
|
13843
|
+
for key in title_keys:
|
|
13844
|
+
current_title = str(value.get(key) or "").strip()
|
|
13845
|
+
if current_title and current_title in by_title:
|
|
13846
|
+
replacement = by_title[current_title]
|
|
13847
|
+
break
|
|
13848
|
+
if replacement is not None:
|
|
13849
|
+
for key in title_keys:
|
|
13850
|
+
current_title = str(value.get(key) or "").strip()
|
|
13851
|
+
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):
|
|
13852
|
+
value[key] = replacement
|
|
13853
|
+
sup_id = _coerce_nonnegative_int(value.get("supId"))
|
|
13854
|
+
if sup_id is not None and sup_id in by_que_id and "supQueTitle" in value:
|
|
13855
|
+
value["supQueTitle"] = by_que_id[sup_id]
|
|
13856
|
+
|
|
13857
|
+
for child_value in value.values():
|
|
13858
|
+
if isinstance(child_value, (dict, list)):
|
|
13859
|
+
_sync_question_title_references(child_value, by_que_id=by_que_id, by_title=by_title)
|
|
13860
|
+
|
|
13861
|
+
|
|
13862
|
+
def _materialize_preserved_subtable_question(field: dict[str, Any], *, template: dict[str, Any]) -> dict[str, Any] | None:
|
|
13863
|
+
materialized_subquestions: list[dict[str, Any]] = []
|
|
13864
|
+
for subfield in cast(list[dict[str, Any]], field.get("subfields") or []):
|
|
13865
|
+
if not isinstance(subfield, dict):
|
|
13866
|
+
continue
|
|
13867
|
+
materialized = _materialize_preserved_question(subfield)
|
|
13868
|
+
if materialized is None:
|
|
13869
|
+
return None
|
|
13870
|
+
materialized_subquestions.append(materialized)
|
|
13871
|
+
template["subQuestions"] = materialized_subquestions
|
|
13872
|
+
template["innerQuestions"] = [deepcopy(materialized_subquestions)]
|
|
13873
|
+
return template
|
|
13874
|
+
|
|
13875
|
+
|
|
13876
|
+
def _materialize_preserved_question(field: dict[str, Any]) -> dict[str, Any] | None:
|
|
13877
|
+
template = deepcopy(field.get("_question_template"))
|
|
13878
|
+
if not isinstance(template, dict):
|
|
13879
|
+
return None
|
|
13880
|
+
overlay_keys = _field_question_overlay_keys(field)
|
|
13881
|
+
if "name" in overlay_keys:
|
|
13882
|
+
template["queTitle"] = str(field.get("name") or "")
|
|
13883
|
+
if "required" in overlay_keys:
|
|
13884
|
+
template["required"] = bool(field.get("required", False))
|
|
13885
|
+
if "description" in overlay_keys:
|
|
13886
|
+
description = field.get("description")
|
|
13887
|
+
template["queHint"] = "" if description is None else str(description)
|
|
13888
|
+
if str(field.get("type") or "") == FieldType.subtable.value:
|
|
13889
|
+
return _materialize_preserved_subtable_question(field, template=template)
|
|
13890
|
+
if str(field.get("type") or "") == FieldType.relation.value:
|
|
13891
|
+
return _normalize_relation_question_for_save(template, field=field)
|
|
13892
|
+
return template
|
|
13893
|
+
|
|
13894
|
+
|
|
13895
|
+
def _materialize_edit_question(field: dict[str, Any], *, temp_id: int) -> tuple[dict[str, Any], bool]:
|
|
13896
|
+
if not _field_needs_question_rebuild(field):
|
|
13897
|
+
preserved = _materialize_preserved_question(field)
|
|
13898
|
+
if preserved is not None:
|
|
13899
|
+
return preserved, True
|
|
13900
|
+
return _field_to_question(field, temp_id=temp_id), False
|
|
13901
|
+
|
|
13902
|
+
|
|
13903
|
+
def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]:
|
|
13904
|
+
built_question, _next_temp_id = build_question(
|
|
13905
|
+
{
|
|
13906
|
+
"label": field["name"],
|
|
13907
|
+
"type": field["type"],
|
|
13908
|
+
"required": field.get("required", False),
|
|
13909
|
+
"description": field.get("description"),
|
|
13910
|
+
"options": field.get("options", []),
|
|
13911
|
+
"target_entity_id": field.get("target_app_key") or "__TARGET_APP_KEY__",
|
|
13912
|
+
"target_field_id": field.get("target_field_id"),
|
|
13913
|
+
"config": deepcopy(field.get("config") or {}),
|
|
13914
|
+
"subfields": [
|
|
13915
|
+
{
|
|
13916
|
+
"label": subfield["name"],
|
|
13917
|
+
"type": subfield["type"],
|
|
13918
|
+
"required": subfield.get("required", False),
|
|
13919
|
+
"description": subfield.get("description"),
|
|
13920
|
+
"options": subfield.get("options", []),
|
|
13921
|
+
"target_entity_id": subfield.get("target_app_key") or "__TARGET_APP_KEY__",
|
|
13922
|
+
"subfields": subfield.get("subfields", []),
|
|
13923
|
+
}
|
|
13924
|
+
for subfield in field.get("subfields", [])
|
|
13925
|
+
],
|
|
13926
|
+
},
|
|
13927
|
+
temp_id,
|
|
13928
|
+
)
|
|
13929
|
+
relation_config_explicit = bool(field.get("_relation_config_explicit"))
|
|
13930
|
+
relation_question_template = (
|
|
13931
|
+
deepcopy(field.get("_question_template"))
|
|
13932
|
+
if field.get("type") == FieldType.relation.value and isinstance(field.get("_question_template"), dict)
|
|
13933
|
+
else None
|
|
13934
|
+
)
|
|
13935
|
+
question = (
|
|
13936
|
+
relation_question_template
|
|
13937
|
+
if relation_question_template is not None and not relation_config_explicit
|
|
13938
|
+
else built_question
|
|
13939
|
+
)
|
|
13940
|
+
if relation_config_explicit and relation_question_template is not None:
|
|
13941
|
+
for key in ("queOriginType", "relationDisplayMode", "customRenderConfig"):
|
|
13942
|
+
if key in relation_question_template:
|
|
13943
|
+
question[key] = deepcopy(relation_question_template[key])
|
|
13944
|
+
if _coerce_nonnegative_int(field.get("que_id")) is not None:
|
|
13945
|
+
question["queId"] = field["que_id"]
|
|
13946
|
+
question.pop("queTempId", None)
|
|
13947
|
+
else:
|
|
13948
|
+
question["queId"] = 0
|
|
13949
|
+
question["queTempId"] = temp_id
|
|
13950
|
+
field["que_temp_id"] = temp_id
|
|
13951
|
+
question["queType"] = built_question.get("queType", question.get("queType"))
|
|
13952
|
+
question["queTitle"] = built_question.get("queTitle", field["name"])
|
|
13953
|
+
question["required"] = built_question.get("required", bool(field.get("required", False)))
|
|
13954
|
+
question["queHint"] = built_question.get("queHint", field.get("description") or "")
|
|
13955
|
+
if field.get("default_type") is not None:
|
|
13956
|
+
question["queDefaultType"] = _coerce_positive_int(field.get("default_type")) or 1
|
|
13957
|
+
if "default_value" in field:
|
|
13958
|
+
question["queDefaultValue"] = field.get("default_value")
|
|
13959
|
+
if field.get("type") == FieldType.relation.value:
|
|
13960
|
+
preserved_reference = (
|
|
13961
|
+
deepcopy(field.get("_reference_config_template"))
|
|
13962
|
+
if not relation_config_explicit and isinstance(field.get("_reference_config_template"), dict)
|
|
13963
|
+
else None
|
|
13964
|
+
)
|
|
13965
|
+
if preserved_reference is not None:
|
|
13966
|
+
preserved_reference["referAppKey"] = field.get("target_app_key")
|
|
13967
|
+
question["referenceConfig"] = preserved_reference
|
|
13968
|
+
else:
|
|
13969
|
+
existing_reference = (
|
|
13970
|
+
deepcopy(relation_question_template.get("referenceConfig"))
|
|
13971
|
+
if relation_question_template is not None and isinstance(relation_question_template.get("referenceConfig"), dict)
|
|
13972
|
+
else deepcopy(question.get("referenceConfig"))
|
|
13973
|
+
if isinstance(question.get("referenceConfig"), dict)
|
|
13974
|
+
else {}
|
|
13975
|
+
)
|
|
13976
|
+
reference = (
|
|
13977
|
+
existing_reference
|
|
13978
|
+
if relation_config_explicit
|
|
13979
|
+
else deepcopy(question.get("referenceConfig"))
|
|
13980
|
+
if isinstance(question.get("referenceConfig"), dict)
|
|
13981
|
+
else {}
|
|
13982
|
+
)
|
|
13983
|
+
built_reference = (
|
|
13984
|
+
deepcopy(built_question.get("referenceConfig"))
|
|
13985
|
+
if isinstance(built_question.get("referenceConfig"), dict)
|
|
13986
|
+
else {}
|
|
13987
|
+
)
|
|
13988
|
+
original_target_app_key = str(existing_reference.get("referAppKey") or "").strip()
|
|
13989
|
+
next_target_app_key = str(field.get("target_app_key") or "").strip()
|
|
13990
|
+
preserve_existing_reference_questions = (
|
|
13991
|
+
relation_config_explicit
|
|
13992
|
+
and bool(original_target_app_key)
|
|
13993
|
+
and original_target_app_key == next_target_app_key
|
|
13994
|
+
)
|
|
13995
|
+
if relation_config_explicit:
|
|
13996
|
+
for stale_key in ("customButtonText", "customAdvancedSetting", "configShowForm", "dataShowForm"):
|
|
13997
|
+
reference.pop(stale_key, None)
|
|
13998
|
+
for key in (
|
|
13999
|
+
"referQueId",
|
|
14000
|
+
"referQuestions",
|
|
14001
|
+
"referAuthQues",
|
|
14002
|
+
"optionalDataNum",
|
|
14003
|
+
"fieldNameShow",
|
|
14004
|
+
"_targetFieldId",
|
|
14005
|
+
):
|
|
14006
|
+
if preserve_existing_reference_questions and key in {"referQuestions", "referAuthQues"}:
|
|
14007
|
+
continue
|
|
14008
|
+
if key in built_reference:
|
|
14009
|
+
reference[key] = deepcopy(built_reference[key])
|
|
14010
|
+
reference["referAppKey"] = field.get("target_app_key")
|
|
14011
|
+
reference["_targetEntityId"] = field.get("target_app_key")
|
|
14012
|
+
if field.get("target_field_que_id") is not None:
|
|
14013
|
+
reference["referQueId"] = field.get("target_field_que_id")
|
|
14014
|
+
question["referenceConfig"] = reference
|
|
14015
|
+
question = _normalize_relation_question_for_save(question, field=field)
|
|
14016
|
+
if field.get("type") == FieldType.department.value:
|
|
14017
|
+
scope_type, scope_payload = _serialize_department_scope_for_question(field.get("department_scope"))
|
|
14018
|
+
question["deptSelectScopeType"] = scope_type
|
|
14019
|
+
question["deptSelectScope"] = scope_payload
|
|
14020
|
+
if field.get("type") == FieldType.code_block.value:
|
|
14021
|
+
code_block_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {}) or {
|
|
14022
|
+
"config_mode": 1,
|
|
14023
|
+
"code_content": "",
|
|
14024
|
+
"being_hide_on_form": False,
|
|
14025
|
+
"result_alias_path": [],
|
|
14026
|
+
}
|
|
14027
|
+
question["codeBlockConfig"] = {
|
|
14028
|
+
"configMode": code_block_config["config_mode"],
|
|
14029
|
+
"codeContent": code_block_config["code_content"],
|
|
14030
|
+
"resultAliasPath": [
|
|
14031
|
+
_serialize_code_block_alias_path_item(item) for item in code_block_config.get("result_alias_path", [])
|
|
14032
|
+
],
|
|
14033
|
+
"beingHideOnForm": code_block_config["being_hide_on_form"],
|
|
14034
|
+
}
|
|
14035
|
+
question["autoTrigger"] = bool(field.get("auto_trigger", False))
|
|
14036
|
+
question["customBtnTextStatus"] = bool(field.get("custom_button_text_enabled", False))
|
|
14037
|
+
question["customBtnText"] = str(field.get("custom_button_text") or "")
|
|
14038
|
+
if field.get("type") == FieldType.q_linker.value:
|
|
14039
|
+
remote_lookup_config = _normalize_remote_lookup_config(field.get("remote_lookup_config") or field.get("config") or {}) or {
|
|
14040
|
+
"config_mode": 1,
|
|
14041
|
+
"url": "",
|
|
14042
|
+
"method": "GET",
|
|
14043
|
+
"headers": [],
|
|
14044
|
+
"body_type": 1,
|
|
14045
|
+
"url_encoded_value": [],
|
|
14046
|
+
"json_value": None,
|
|
14047
|
+
"xml_value": None,
|
|
14048
|
+
"result_type": 1,
|
|
14049
|
+
"result_format_path": [],
|
|
14050
|
+
"query_params": [],
|
|
14051
|
+
"auto_trigger": None,
|
|
14052
|
+
"custom_button_text_enabled": None,
|
|
14053
|
+
"custom_button_text": None,
|
|
14054
|
+
"being_insert_value_directly": None,
|
|
14055
|
+
"being_hide_on_form": None,
|
|
14056
|
+
}
|
|
14057
|
+
question["remoteLookupConfig"] = {
|
|
14058
|
+
"configMode": remote_lookup_config["config_mode"],
|
|
14059
|
+
"url": remote_lookup_config["url"],
|
|
14060
|
+
"method": remote_lookup_config["method"],
|
|
14061
|
+
"headers": [_serialize_remote_lookup_key_value_item(item) for item in remote_lookup_config.get("headers", [])],
|
|
14062
|
+
"bodyType": remote_lookup_config["body_type"],
|
|
14063
|
+
"urlEncodedValue": [_serialize_remote_lookup_key_value_item(item) for item in remote_lookup_config.get("url_encoded_value", [])],
|
|
14064
|
+
"jsonValue": remote_lookup_config.get("json_value"),
|
|
14065
|
+
"xmlValue": remote_lookup_config.get("xml_value"),
|
|
14066
|
+
"resultType": remote_lookup_config["result_type"],
|
|
14067
|
+
"resultFormatPath": [_serialize_q_linker_alias_path_item(item) for item in remote_lookup_config.get("result_format_path", [])],
|
|
14068
|
+
"queryParams": [_serialize_remote_lookup_key_value_item(item) for item in remote_lookup_config.get("query_params", [])],
|
|
14069
|
+
"beingInsertValueDirectly": bool(remote_lookup_config.get("being_insert_value_directly", False)),
|
|
14070
|
+
"beingHideOnForm": bool(remote_lookup_config.get("being_hide_on_form", False)),
|
|
14071
|
+
}
|
|
14072
|
+
if remote_lookup_config["config_mode"] == 1:
|
|
14073
|
+
question["remoteLookupConfig"]["openAppConfig"] = {"event": {"eventId": 0, "name": "custom"}}
|
|
14074
|
+
question["autoTrigger"] = bool(field.get("auto_trigger", remote_lookup_config.get("auto_trigger", False)))
|
|
14075
|
+
question["customBtnTextStatus"] = bool(field.get("custom_button_text_enabled", remote_lookup_config.get("custom_button_text_enabled", False)))
|
|
12806
14076
|
question["customBtnText"] = str(field.get("custom_button_text") or remote_lookup_config.get("custom_button_text") or "")
|
|
12807
14077
|
return question
|
|
12808
14078
|
|
|
@@ -12882,8 +14152,127 @@ def _build_form_payload_from_fields(
|
|
|
12882
14152
|
for row in form_rows:
|
|
12883
14153
|
_apply_row_widths(row)
|
|
12884
14154
|
payload = default_form_payload(title, form_rows)
|
|
14155
|
+
_normalize_formula_defaults_for_save(payload.get("formQues"))
|
|
14156
|
+
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
14157
|
+
payload["questionRelations"] = _normalize_question_relations_for_save(
|
|
14158
|
+
question_relations if question_relations is not None else (current_schema.get("questionRelations") or [])
|
|
14159
|
+
)
|
|
14160
|
+
return payload
|
|
14161
|
+
|
|
14162
|
+
|
|
14163
|
+
def _build_form_payload_for_edit_fields(
|
|
14164
|
+
*,
|
|
14165
|
+
title: str,
|
|
14166
|
+
current_schema: dict[str, Any],
|
|
14167
|
+
fields: list[dict[str, Any]],
|
|
14168
|
+
layout: dict[str, Any],
|
|
14169
|
+
question_relations: list[dict[str, Any]] | None = None,
|
|
14170
|
+
) -> dict[str, Any]:
|
|
14171
|
+
_, section_templates = _extract_question_templates(current_schema)
|
|
14172
|
+
template_row_lengths_by_que_id, template_row_lengths_by_title = _extract_template_row_lengths(current_schema)
|
|
14173
|
+
fields_by_name = {
|
|
14174
|
+
str(field.get("name") or ""): field
|
|
14175
|
+
for field in fields
|
|
14176
|
+
if isinstance(field, dict) and str(field.get("name") or "").strip()
|
|
14177
|
+
}
|
|
14178
|
+
form_rows: list[list[dict[str, Any]]] = []
|
|
14179
|
+
temp_id = -10000
|
|
14180
|
+
|
|
14181
|
+
for row in layout.get("root_rows", []) or []:
|
|
14182
|
+
questions: list[dict[str, Any]] = []
|
|
14183
|
+
expected_template_lengths: list[int] = []
|
|
14184
|
+
row_preserved = True
|
|
14185
|
+
for name in row:
|
|
14186
|
+
field = fields_by_name.get(str(name))
|
|
14187
|
+
if field is None:
|
|
14188
|
+
continue
|
|
14189
|
+
template_row_length = _field_template_row_length(
|
|
14190
|
+
field,
|
|
14191
|
+
lengths_by_que_id=template_row_lengths_by_que_id,
|
|
14192
|
+
lengths_by_title=template_row_lengths_by_title,
|
|
14193
|
+
)
|
|
14194
|
+
if template_row_length is not None:
|
|
14195
|
+
expected_template_lengths.append(template_row_length)
|
|
14196
|
+
question, preserved = _materialize_edit_question(field, temp_id=temp_id)
|
|
14197
|
+
questions.append(question)
|
|
14198
|
+
row_preserved = row_preserved and preserved
|
|
14199
|
+
temp_id -= 100
|
|
14200
|
+
if not questions:
|
|
14201
|
+
continue
|
|
14202
|
+
if not row_preserved or _row_needs_width_reflow(expected_template_lengths, len(questions)):
|
|
14203
|
+
_apply_row_widths(questions)
|
|
14204
|
+
form_rows.append(questions)
|
|
14205
|
+
|
|
14206
|
+
for section in layout.get("sections", []) or []:
|
|
14207
|
+
inner_rows: list[list[dict[str, Any]]] = []
|
|
14208
|
+
for row in section.get("rows", []) or []:
|
|
14209
|
+
questions: list[dict[str, Any]] = []
|
|
14210
|
+
expected_template_lengths: list[int] = []
|
|
14211
|
+
row_preserved = True
|
|
14212
|
+
for name in row:
|
|
14213
|
+
field = fields_by_name.get(str(name))
|
|
14214
|
+
if field is None:
|
|
14215
|
+
continue
|
|
14216
|
+
template_row_length = _field_template_row_length(
|
|
14217
|
+
field,
|
|
14218
|
+
lengths_by_que_id=template_row_lengths_by_que_id,
|
|
14219
|
+
lengths_by_title=template_row_lengths_by_title,
|
|
14220
|
+
)
|
|
14221
|
+
if template_row_length is not None:
|
|
14222
|
+
expected_template_lengths.append(template_row_length)
|
|
14223
|
+
question, preserved = _materialize_edit_question(field, temp_id=temp_id)
|
|
14224
|
+
questions.append(question)
|
|
14225
|
+
row_preserved = row_preserved and preserved
|
|
14226
|
+
temp_id -= 100
|
|
14227
|
+
if not questions:
|
|
14228
|
+
continue
|
|
14229
|
+
if not row_preserved or _row_needs_width_reflow(expected_template_lengths, len(questions)):
|
|
14230
|
+
_apply_row_widths(questions)
|
|
14231
|
+
inner_rows.append(questions)
|
|
14232
|
+
if not inner_rows:
|
|
14233
|
+
continue
|
|
14234
|
+
template = _select_section_template(section_templates, section)
|
|
14235
|
+
wrapper = deepcopy(template) if isinstance(template, dict) else {
|
|
14236
|
+
"queId": 0,
|
|
14237
|
+
"queTempId": -(20000 + sum(ord(ch) for ch in str(section.get("section_id") or section.get("title") or "section"))),
|
|
14238
|
+
"queType": 24,
|
|
14239
|
+
"queWidth": 100,
|
|
14240
|
+
"scanType": 1,
|
|
14241
|
+
"status": 1,
|
|
14242
|
+
"required": False,
|
|
14243
|
+
"queHint": "",
|
|
14244
|
+
"linkedQuestions": {},
|
|
14245
|
+
"logicalShow": True,
|
|
14246
|
+
"queDefaultValue": None,
|
|
14247
|
+
"queDefaultType": 1,
|
|
14248
|
+
"subQueWidth": 2,
|
|
14249
|
+
"beingHide": False,
|
|
14250
|
+
"beingDesensitized": False,
|
|
14251
|
+
}
|
|
14252
|
+
if section.get("title") is not None:
|
|
14253
|
+
wrapper["queTitle"] = section.get("title") or wrapper.get("queTitle") or "未命名分组"
|
|
14254
|
+
parsed_section_id = _coerce_positive_int(section.get("section_id"))
|
|
14255
|
+
if parsed_section_id is not None:
|
|
14256
|
+
wrapper["sectionId"] = parsed_section_id
|
|
14257
|
+
elif template is None and section.get("section_id") is not None:
|
|
14258
|
+
wrapper["sectionId"] = section.get("section_id")
|
|
14259
|
+
wrapper["innerQuestions"] = inner_rows
|
|
14260
|
+
form_rows.append([wrapper])
|
|
14261
|
+
|
|
14262
|
+
rename_by_que_id, rename_by_title = _field_rename_maps(fields)
|
|
14263
|
+
if rename_by_que_id or rename_by_title:
|
|
14264
|
+
_sync_question_title_references(form_rows, by_que_id=rename_by_que_id, by_title=rename_by_title)
|
|
14265
|
+
normalized_relations = _normalize_question_relations_for_save(
|
|
14266
|
+
question_relations if question_relations is not None else (current_schema.get("questionRelations") or [])
|
|
14267
|
+
)
|
|
14268
|
+
if rename_by_que_id or rename_by_title:
|
|
14269
|
+
_sync_question_title_references(normalized_relations, by_que_id=rename_by_que_id, by_title=rename_by_title)
|
|
14270
|
+
|
|
14271
|
+
payload = _build_form_save_base_payload(current_schema, title)
|
|
14272
|
+
payload["formQues"] = form_rows
|
|
14273
|
+
_normalize_formula_defaults_for_save(payload.get("formQues"))
|
|
14274
|
+
payload["questionRelations"] = normalized_relations
|
|
12885
14275
|
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
14276
|
return payload
|
|
12888
14277
|
|
|
12889
14278
|
|
|
@@ -13048,6 +14437,8 @@ def _summarize_views(result: Any) -> list[dict[str, Any]]:
|
|
|
13048
14437
|
view_type = _normalize_view_type_name(view.get("viewgraphType") or view.get("type"))
|
|
13049
14438
|
columns = view.get("columnNames") or view.get("columns") or []
|
|
13050
14439
|
group_by = view.get("groupBy") or view.get("group_by")
|
|
14440
|
+
if not any((name, view_type, columns, group_by)) and str(view_key or "").isdigit():
|
|
14441
|
+
continue
|
|
13051
14442
|
if not any((name, view_key, view_type, columns, group_by)):
|
|
13052
14443
|
continue
|
|
13053
14444
|
items.append(
|
|
@@ -13090,11 +14481,24 @@ def _summarize_views_with_config(views_tool: ViewTools, *, profile: str, views:
|
|
|
13090
14481
|
enriched_items.append(item)
|
|
13091
14482
|
continue
|
|
13092
14483
|
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
13093
|
-
|
|
14484
|
+
question_list: list[dict[str, Any]] = []
|
|
14485
|
+
try:
|
|
14486
|
+
question_response = views_tool.view_list_questions(profile=profile, viewgraph_key=view_key)
|
|
14487
|
+
raw_question_list = question_response.get("result")
|
|
14488
|
+
if isinstance(raw_question_list, list):
|
|
14489
|
+
question_list = [deepcopy(entry) for entry in raw_question_list if isinstance(entry, dict)]
|
|
14490
|
+
except (QingflowApiError, RuntimeError):
|
|
14491
|
+
question_list = []
|
|
14492
|
+
enriched_items.append(_merge_view_summary_with_config(item, config=config, question_list=question_list))
|
|
13094
14493
|
return enriched_items, config_read_errors
|
|
13095
14494
|
|
|
13096
14495
|
|
|
13097
|
-
def _merge_view_summary_with_config(
|
|
14496
|
+
def _merge_view_summary_with_config(
|
|
14497
|
+
base: dict[str, Any],
|
|
14498
|
+
*,
|
|
14499
|
+
config: dict[str, Any],
|
|
14500
|
+
question_list: list[dict[str, Any]] | None = None,
|
|
14501
|
+
) -> dict[str, Any]:
|
|
13098
14502
|
summary = deepcopy(base)
|
|
13099
14503
|
if not isinstance(config, dict) or not config:
|
|
13100
14504
|
return summary
|
|
@@ -13102,6 +14506,7 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
13102
14506
|
summary["visibility_summary"] = _visibility_summary(_public_visibility_from_member_auth(config.get("auth")))
|
|
13103
14507
|
legacy_columns = [str(value) for value in (summary.get("columns") or []) if str(value or "").strip()]
|
|
13104
14508
|
question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
|
|
14509
|
+
canonical_question_entries = _extract_view_question_entries(question_list)
|
|
13105
14510
|
question_entries_by_id = {
|
|
13106
14511
|
field_id: entry
|
|
13107
14512
|
for entry in question_entries
|
|
@@ -13121,19 +14526,20 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
13121
14526
|
display_entries = _sort_view_question_entries(
|
|
13122
14527
|
[entry for entry in question_entries if bool(entry.get("visible", True))],
|
|
13123
14528
|
)
|
|
14529
|
+
public_display_entries = _filter_public_view_display_entries(display_entries, configured_column_ids=configured_column_ids)
|
|
13124
14530
|
display_column_ids = [
|
|
13125
14531
|
field_id
|
|
13126
|
-
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in
|
|
14532
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in public_display_entries)
|
|
13127
14533
|
if field_id is not None
|
|
13128
14534
|
]
|
|
13129
14535
|
display_columns = [
|
|
13130
14536
|
str(entry.get("name") or "").strip()
|
|
13131
|
-
for entry in
|
|
14537
|
+
for entry in public_display_entries
|
|
13132
14538
|
if str(entry.get("name") or "").strip()
|
|
13133
14539
|
]
|
|
13134
14540
|
apply_entries = [
|
|
13135
14541
|
entry
|
|
13136
|
-
for entry in
|
|
14542
|
+
for entry in public_display_entries
|
|
13137
14543
|
if _coerce_nonnegative_int(entry.get("field_id")) is not None
|
|
13138
14544
|
and str(entry.get("name") or "").strip()
|
|
13139
14545
|
and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
@@ -13180,8 +14586,42 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
13180
14586
|
summary["apply_columns"] = apply_columns
|
|
13181
14587
|
summary["apply_column_ids"] = apply_column_ids
|
|
13182
14588
|
config_enriched = True
|
|
13183
|
-
if
|
|
13184
|
-
|
|
14589
|
+
if canonical_question_entries:
|
|
14590
|
+
canonical_display_entries = _sort_view_question_entries(canonical_question_entries)
|
|
14591
|
+
canonical_display_column_ids = [
|
|
14592
|
+
field_id
|
|
14593
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in canonical_display_entries)
|
|
14594
|
+
if field_id is not None
|
|
14595
|
+
]
|
|
14596
|
+
canonical_display_columns = [
|
|
14597
|
+
str(entry.get("name") or "").strip()
|
|
14598
|
+
for entry in canonical_display_entries
|
|
14599
|
+
if str(entry.get("name") or "").strip()
|
|
14600
|
+
]
|
|
14601
|
+
canonical_apply_entries = [
|
|
14602
|
+
entry
|
|
14603
|
+
for entry in canonical_display_entries
|
|
14604
|
+
if _coerce_nonnegative_int(entry.get("field_id")) is not None
|
|
14605
|
+
and str(entry.get("name") or "").strip()
|
|
14606
|
+
and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
14607
|
+
]
|
|
14608
|
+
summary["columns"] = canonical_display_columns
|
|
14609
|
+
summary["display_columns"] = canonical_display_columns
|
|
14610
|
+
summary["display_column_ids"] = canonical_display_column_ids
|
|
14611
|
+
summary["column_details"] = canonical_display_entries
|
|
14612
|
+
summary["apply_columns"] = [
|
|
14613
|
+
str(entry.get("name") or "").strip()
|
|
14614
|
+
for entry in canonical_apply_entries
|
|
14615
|
+
if str(entry.get("name") or "").strip()
|
|
14616
|
+
]
|
|
14617
|
+
summary["apply_column_ids"] = [
|
|
14618
|
+
field_id
|
|
14619
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in canonical_apply_entries)
|
|
14620
|
+
if field_id is not None
|
|
14621
|
+
]
|
|
14622
|
+
config_enriched = True
|
|
14623
|
+
elif question_entries:
|
|
14624
|
+
summary["column_details"] = public_display_entries or _sort_view_question_entries(question_entries)
|
|
13185
14625
|
config_enriched = True
|
|
13186
14626
|
display_config = _extract_view_display_config(
|
|
13187
14627
|
config,
|
|
@@ -13206,7 +14646,7 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
13206
14646
|
summary["button_read_source"] = button_source
|
|
13207
14647
|
config_enriched = True
|
|
13208
14648
|
if config_enriched:
|
|
13209
|
-
summary["read_source"] = "view_config"
|
|
14649
|
+
summary["read_source"] = "view_config+question" if canonical_question_entries else "view_config"
|
|
13210
14650
|
return summary
|
|
13211
14651
|
|
|
13212
14652
|
|
|
@@ -13214,29 +14654,64 @@ def _extract_view_question_entries(questions: Any) -> list[dict[str, Any]]:
|
|
|
13214
14654
|
if not isinstance(questions, list):
|
|
13215
14655
|
return []
|
|
13216
14656
|
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
|
-
|
|
14657
|
+
fallback_order = 0
|
|
14658
|
+
|
|
14659
|
+
def walk(nodes: Any) -> None:
|
|
14660
|
+
nonlocal fallback_order
|
|
14661
|
+
if not isinstance(nodes, list):
|
|
14662
|
+
return
|
|
14663
|
+
for item in nodes:
|
|
14664
|
+
if not isinstance(item, dict):
|
|
14665
|
+
continue
|
|
14666
|
+
children: list[Any] = []
|
|
14667
|
+
for child_key in ("innerQues", "subQues", "innerQuestions", "subQuestions"):
|
|
14668
|
+
child_value = item.get(child_key)
|
|
14669
|
+
if isinstance(child_value, list) and child_value:
|
|
14670
|
+
children.extend(child_value)
|
|
14671
|
+
if children:
|
|
14672
|
+
walk(children)
|
|
14673
|
+
continue
|
|
14674
|
+
field_id = _coerce_nonnegative_int(item.get("queId"))
|
|
14675
|
+
name = str(item.get("queTitle") or "").strip() or None
|
|
14676
|
+
if field_id is None and name is None:
|
|
14677
|
+
continue
|
|
14678
|
+
visible_raw = item.get("beingListDisplay")
|
|
14679
|
+
if visible_raw is None:
|
|
14680
|
+
visible_raw = item.get("beingVisible")
|
|
14681
|
+
visible = bool(visible_raw) if visible_raw is not None else True
|
|
14682
|
+
display_order = _coerce_positive_int(item.get("displayOrdinal"))
|
|
14683
|
+
fallback_order += 1
|
|
14684
|
+
entry: dict[str, Any] = {
|
|
14685
|
+
"field_id": field_id,
|
|
14686
|
+
"name": name,
|
|
14687
|
+
"visible": visible,
|
|
14688
|
+
"display_order": display_order if display_order is not None else fallback_order,
|
|
14689
|
+
}
|
|
14690
|
+
width = _coerce_positive_int(item.get("width"))
|
|
14691
|
+
if width is not None:
|
|
14692
|
+
entry["width"] = width
|
|
14693
|
+
entries.append(entry)
|
|
14694
|
+
|
|
14695
|
+
walk(questions)
|
|
13237
14696
|
return entries
|
|
13238
14697
|
|
|
13239
14698
|
|
|
14699
|
+
def _filter_public_view_display_entries(
|
|
14700
|
+
entries: list[dict[str, Any]],
|
|
14701
|
+
*,
|
|
14702
|
+
configured_column_ids: list[int],
|
|
14703
|
+
) -> list[dict[str, Any]]:
|
|
14704
|
+
configured_set = set(configured_column_ids)
|
|
14705
|
+
filtered: list[dict[str, Any]] = []
|
|
14706
|
+
for entry in entries:
|
|
14707
|
+
name = str(entry.get("name") or "").strip()
|
|
14708
|
+
field_id = _coerce_nonnegative_int(entry.get("field_id"))
|
|
14709
|
+
if name in _KNOWN_SYSTEM_VIEW_COLUMNS and field_id not in configured_set:
|
|
14710
|
+
continue
|
|
14711
|
+
filtered.append(entry)
|
|
14712
|
+
return filtered or entries
|
|
14713
|
+
|
|
14714
|
+
|
|
13240
14715
|
def _sort_view_question_entries(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
13241
14716
|
return sorted(
|
|
13242
14717
|
entries,
|
|
@@ -13430,7 +14905,6 @@ def _normalize_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
|
13430
14905
|
for public_key, source_key in (
|
|
13431
14906
|
("default_button_text", "defaultButtonText"),
|
|
13432
14907
|
("button_icon", "buttonIcon"),
|
|
13433
|
-
("icon_color", "iconColor"),
|
|
13434
14908
|
("background_color", "backgroundColor"),
|
|
13435
14909
|
("text_color", "textColor"),
|
|
13436
14910
|
("trigger_link_url", "triggerLinkUrl"),
|
|
@@ -13468,7 +14942,6 @@ def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
|
|
|
13468
14942
|
"button_id": normalized.get("button_id") if is_custom else None,
|
|
13469
14943
|
"button_text": normalized.get("button_text"),
|
|
13470
14944
|
"button_icon": normalized.get("button_icon"),
|
|
13471
|
-
"icon_color": normalized.get("icon_color"),
|
|
13472
14945
|
"background_color": normalized.get("background_color"),
|
|
13473
14946
|
"text_color": normalized.get("text_color"),
|
|
13474
14947
|
"trigger_action": normalized.get("trigger_action"),
|
|
@@ -13543,7 +15016,6 @@ def _normalize_expected_view_buttons_for_compare(
|
|
|
13543
15016
|
for key in (
|
|
13544
15017
|
"button_text",
|
|
13545
15018
|
"button_icon",
|
|
13546
|
-
"icon_color",
|
|
13547
15019
|
"background_color",
|
|
13548
15020
|
"text_color",
|
|
13549
15021
|
"trigger_action",
|
|
@@ -13726,8 +15198,6 @@ def _serialize_view_button_binding(
|
|
|
13726
15198
|
if binding.button_type in {PublicViewButtonType.system, PublicViewButtonType.custom}:
|
|
13727
15199
|
dto["buttonText"] = binding.button_text
|
|
13728
15200
|
dto["buttonIcon"] = binding.button_icon
|
|
13729
|
-
if str(binding.icon_color or "").strip():
|
|
13730
|
-
dto["iconColor"] = binding.icon_color
|
|
13731
15201
|
dto["backgroundColor"] = binding.background_color
|
|
13732
15202
|
dto["textColor"] = binding.text_color
|
|
13733
15203
|
dto["triggerAction"] = binding.trigger_action
|
|
@@ -14296,9 +15766,10 @@ def _build_form_payload_from_existing_schema(
|
|
|
14296
15766
|
wrapper["queWidth"] = 100
|
|
14297
15767
|
form_rows.append([wrapper])
|
|
14298
15768
|
|
|
14299
|
-
payload =
|
|
15769
|
+
payload = _build_form_save_base_payload(current_schema, str(current_schema.get("formTitle") or "未命名应用"))
|
|
14300
15770
|
payload["formQues"] = form_rows
|
|
14301
|
-
payload
|
|
15771
|
+
_normalize_formula_defaults_for_save(payload.get("formQues"))
|
|
15772
|
+
payload["questionRelations"] = _normalize_question_relations_for_save(current_schema.get("questionRelations") or [])
|
|
14302
15773
|
payload["editVersionNo"] = int(current_schema.get("editVersionNo") or 1)
|
|
14303
15774
|
payload.setdefault("formTitle", current_schema.get("formTitle") or "未命名应用")
|
|
14304
15775
|
return payload
|