@josephyan/qingflow-cli 0.2.0-beta.55
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 +30 -0
- package/docs/local-agent-install.md +235 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +204 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +547 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +985 -0
- package/src/qingflow_mcp/builder_facade/service.py +8243 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +184 -0
- package/src/qingflow_mcp/cli/commands/common.py +47 -0
- package/src/qingflow_mcp/cli/commands/imports.py +86 -0
- package/src/qingflow_mcp/cli/commands/record.py +202 -0
- package/src/qingflow_mcp/cli/commands/task.py +87 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +269 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +147 -0
- package/src/qingflow_mcp/config.py +221 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/server.py +211 -0
- package/src/qingflow_mcp/server_app_builder.py +387 -0
- package/src/qingflow_mcp/server_app_user.py +317 -0
- package/src/qingflow_mcp/session_store.py +289 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2339 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +853 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
- package/src/qingflow_mcp/tools/app_tools.py +850 -0
- package/src/qingflow_mcp/tools/approval_tools.py +833 -0
- package/src/qingflow_mcp/tools/auth_tools.py +697 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
- package/src/qingflow_mcp/tools/directory_tools.py +648 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +385 -0
- package/src/qingflow_mcp/tools/import_tools.py +1971 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +240 -0
- package/src/qingflow_mcp/tools/portal_tools.py +131 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
- package/src/qingflow_mcp/tools/record_tools.py +12739 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
- package/src/qingflow_mcp/tools/task_tools.py +843 -0
- package/src/qingflow_mcp/tools/view_tools.py +280 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
|
@@ -0,0 +1,2339 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from ..errors import QingflowApiError
|
|
9
|
+
from ..tools.app_tools import AppTools
|
|
10
|
+
from ..tools.navigation_tools import NavigationTools
|
|
11
|
+
from ..tools.package_tools import PackageTools
|
|
12
|
+
from ..tools.portal_tools import PortalTools
|
|
13
|
+
from ..tools.qingbi_report_tools import QingbiReportTools
|
|
14
|
+
from ..tools.record_tools import RecordTools
|
|
15
|
+
from ..tools.role_tools import RoleTools
|
|
16
|
+
from ..tools.view_tools import ViewTools
|
|
17
|
+
from ..tools.workflow_tools import WorkflowTools
|
|
18
|
+
from ..tools.workspace_tools import WorkspaceTools
|
|
19
|
+
from .compiler import CompiledEntity, CompiledRole, CompiledSolution
|
|
20
|
+
from .compiler.form_compiler import QUESTION_TYPE_MAP
|
|
21
|
+
from .run_store import RunArtifactStore, fingerprint_payload
|
|
22
|
+
from .spec_models import FieldType
|
|
23
|
+
|
|
24
|
+
NAVIGATION_PLUGIN_ID = 45
|
|
25
|
+
GRID_COMPONENT_PLUGIN_ID = 34
|
|
26
|
+
BI_CHART_COMPONENT_TYPE = 9
|
|
27
|
+
PACKAGE_ITEM_TYPE_FORM = 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SolutionExecutor:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
workspace_tools: WorkspaceTools,
|
|
35
|
+
package_tools: PackageTools,
|
|
36
|
+
role_tools: RoleTools,
|
|
37
|
+
app_tools: AppTools,
|
|
38
|
+
record_tools: RecordTools,
|
|
39
|
+
workflow_tools: WorkflowTools,
|
|
40
|
+
view_tools: ViewTools,
|
|
41
|
+
chart_tools: QingbiReportTools,
|
|
42
|
+
portal_tools: PortalTools,
|
|
43
|
+
navigation_tools: NavigationTools,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.workspace_tools = workspace_tools
|
|
46
|
+
self.package_tools = package_tools
|
|
47
|
+
self.role_tools = role_tools
|
|
48
|
+
self.app_tools = app_tools
|
|
49
|
+
self.record_tools = record_tools
|
|
50
|
+
self.workflow_tools = workflow_tools
|
|
51
|
+
self.view_tools = view_tools
|
|
52
|
+
self.chart_tools = chart_tools
|
|
53
|
+
self.portal_tools = portal_tools
|
|
54
|
+
self.navigation_tools = navigation_tools
|
|
55
|
+
|
|
56
|
+
def execute(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
profile: str,
|
|
60
|
+
compiled: CompiledSolution,
|
|
61
|
+
store: RunArtifactStore,
|
|
62
|
+
publish: bool,
|
|
63
|
+
mode: str,
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
self._current_store = store
|
|
66
|
+
force_from_index = self._repair_start_index(compiled, store) if mode == "repair" else None
|
|
67
|
+
for index, step in enumerate(compiled.execution_plan.steps):
|
|
68
|
+
force = force_from_index is not None and index >= force_from_index
|
|
69
|
+
if not store.should_run(step.step_name, force=force):
|
|
70
|
+
continue
|
|
71
|
+
debug_context = self._step_debug_context(compiled, step.step_name)
|
|
72
|
+
try:
|
|
73
|
+
store.record_step_started(step.step_name, store.data["request_fingerprint"], debug_context=debug_context)
|
|
74
|
+
self._execute_step(profile=profile, compiled=compiled, store=store, step_name=step.step_name, publish=publish)
|
|
75
|
+
store.record_step_completed(step.step_name, debug_context=debug_context)
|
|
76
|
+
except Exception as exc: # noqa: BLE001
|
|
77
|
+
store.record_step_failed(step.step_name, str(exc), debug_context=debug_context)
|
|
78
|
+
return store.summary()
|
|
79
|
+
store.mark_finished(status="success")
|
|
80
|
+
return store.summary()
|
|
81
|
+
|
|
82
|
+
def _repair_start_index(self, compiled: CompiledSolution, store: RunArtifactStore) -> int:
|
|
83
|
+
for index, step in enumerate(compiled.execution_plan.steps):
|
|
84
|
+
if store.get_step_status(step.step_name) != "completed":
|
|
85
|
+
return index
|
|
86
|
+
return len(compiled.execution_plan.steps)
|
|
87
|
+
|
|
88
|
+
def _step_debug_context(self, compiled: CompiledSolution, step_name: str) -> dict[str, Any]:
|
|
89
|
+
context: dict[str, Any] = {"step_name": step_name}
|
|
90
|
+
if step_name == "package.create":
|
|
91
|
+
context["resource"] = "package"
|
|
92
|
+
context["package_name"] = compiled.normalized_spec.package.name or compiled.normalized_spec.solution_name
|
|
93
|
+
return context
|
|
94
|
+
if step_name.startswith("package.attach."):
|
|
95
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
96
|
+
context.update(
|
|
97
|
+
{
|
|
98
|
+
"resource": "package_attach",
|
|
99
|
+
"entity_id": entity.entity_id,
|
|
100
|
+
"display_name": entity.display_name,
|
|
101
|
+
"package_tag_id": self._current_store.get_artifact("package", "tag_id") if hasattr(self, "_current_store") else None,
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
return context
|
|
105
|
+
if step_name == "portal.create":
|
|
106
|
+
context["resource"] = "portal"
|
|
107
|
+
context["portal_name"] = compiled.normalized_spec.portal.name
|
|
108
|
+
context["section_count"] = len(compiled.normalized_spec.portal.sections)
|
|
109
|
+
return context
|
|
110
|
+
if step_name == "navigation.create":
|
|
111
|
+
context["resource"] = "navigation"
|
|
112
|
+
context["item_count"] = len(compiled.normalized_spec.navigation.items)
|
|
113
|
+
return context
|
|
114
|
+
if ".create." in step_name and step_name.startswith("role.create."):
|
|
115
|
+
role = self._role_from_step(compiled, step_name)
|
|
116
|
+
context["resource"] = "role"
|
|
117
|
+
context["role_id"] = role.role_id
|
|
118
|
+
context["role_name"] = role.name
|
|
119
|
+
return context
|
|
120
|
+
if any(step_name.startswith(prefix) for prefix in ("app.create.", "form.base.", "form.relations.", "workflow.", "views.", "charts.", "seed_data.", "publish.form.", "publish.workflow.", "publish.app.")):
|
|
121
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
122
|
+
context.update(
|
|
123
|
+
{
|
|
124
|
+
"resource": "entity",
|
|
125
|
+
"entity_id": entity.entity_id,
|
|
126
|
+
"display_name": entity.display_name,
|
|
127
|
+
"workflow_action_count": len((entity.workflow_plan or {}).get("actions", [])),
|
|
128
|
+
"view_count": len(entity.view_plans),
|
|
129
|
+
"chart_count": len(entity.chart_plans),
|
|
130
|
+
"sample_record_count": len(entity.sample_records),
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
return context
|
|
134
|
+
return context
|
|
135
|
+
|
|
136
|
+
def _execute_step(self, *, profile: str, compiled: CompiledSolution, store: RunArtifactStore, step_name: str, publish: bool) -> None:
|
|
137
|
+
if step_name == "package.create":
|
|
138
|
+
self._create_package(profile, compiled, store)
|
|
139
|
+
return
|
|
140
|
+
if step_name.startswith("package.attach."):
|
|
141
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
142
|
+
self._attach_app_to_package(profile, entity, store)
|
|
143
|
+
return
|
|
144
|
+
if step_name.startswith("role.create."):
|
|
145
|
+
role = self._role_from_step(compiled, step_name)
|
|
146
|
+
self._create_role(profile, role, store)
|
|
147
|
+
return
|
|
148
|
+
if step_name.startswith("app.create."):
|
|
149
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
150
|
+
self._create_app(profile, entity, store)
|
|
151
|
+
return
|
|
152
|
+
if step_name.startswith("form.base."):
|
|
153
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
154
|
+
self._update_form_base(profile, entity, store)
|
|
155
|
+
return
|
|
156
|
+
if step_name.startswith("form.relations."):
|
|
157
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
158
|
+
self._update_form_relations(profile, entity, store)
|
|
159
|
+
return
|
|
160
|
+
if step_name.startswith("workflow."):
|
|
161
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
162
|
+
self._build_workflow(profile, entity, store)
|
|
163
|
+
return
|
|
164
|
+
if step_name.startswith("views."):
|
|
165
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
166
|
+
self._build_views(profile, entity, store)
|
|
167
|
+
return
|
|
168
|
+
if step_name.startswith("charts."):
|
|
169
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
170
|
+
self._build_charts(profile, entity, store)
|
|
171
|
+
return
|
|
172
|
+
if step_name.startswith("seed_data."):
|
|
173
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
174
|
+
self._seed_records(profile, entity, store)
|
|
175
|
+
return
|
|
176
|
+
if step_name == "portal.create":
|
|
177
|
+
self._build_portal(profile, compiled, store)
|
|
178
|
+
return
|
|
179
|
+
if step_name == "navigation.create":
|
|
180
|
+
self._build_navigation(profile, compiled, store)
|
|
181
|
+
return
|
|
182
|
+
if (
|
|
183
|
+
step_name.startswith("publish.form.")
|
|
184
|
+
or step_name.startswith("publish.workflow.")
|
|
185
|
+
or step_name.startswith("publish.app.")
|
|
186
|
+
) and publish and compiled.normalized_spec.publish_policy.apps:
|
|
187
|
+
entity = self._entity_from_step(compiled, step_name)
|
|
188
|
+
app_key = self._get_app_key(store, entity.entity_id)
|
|
189
|
+
edit_version_no = self._get_edit_version_no(store, entity.entity_id)
|
|
190
|
+
self.app_tools.app_publish(
|
|
191
|
+
profile=profile,
|
|
192
|
+
app_key=app_key,
|
|
193
|
+
payload=self._publish_payload(store, entity.entity_id),
|
|
194
|
+
)
|
|
195
|
+
if edit_version_no is not None:
|
|
196
|
+
self.app_tools.app_edit_finished(
|
|
197
|
+
profile=profile,
|
|
198
|
+
app_key=app_key,
|
|
199
|
+
payload={"editVersionNo": int(edit_version_no)},
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
if step_name == "publish.portal" and publish and compiled.normalized_spec.publish_policy.portal:
|
|
203
|
+
dash_key = store.get_artifact("portal", "dash_key")
|
|
204
|
+
if dash_key:
|
|
205
|
+
self.portal_tools.portal_publish(profile=profile, dash_key=dash_key)
|
|
206
|
+
self._refresh_portal_artifact(profile=profile, store=store, being_draft=False, artifact_key="published_result")
|
|
207
|
+
return
|
|
208
|
+
if step_name == "publish.navigation" and publish and compiled.normalized_spec.publish_policy.navigation:
|
|
209
|
+
if store.get_artifact("navigation", "skipped"):
|
|
210
|
+
return
|
|
211
|
+
status = self.navigation_tools.navigation_get_status(profile=profile)
|
|
212
|
+
navigation_status = (status.get("result") or {}) if isinstance(status.get("result"), dict) else {}
|
|
213
|
+
navigation_id = navigation_status.get("navigationId") or navigation_status.get("id")
|
|
214
|
+
if navigation_id:
|
|
215
|
+
self.navigation_tools.navigation_publish(profile=profile, navigation_id=navigation_id)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
def _create_package(self, profile: str, compiled: CompiledSolution, store: RunArtifactStore) -> None:
|
|
219
|
+
if compiled.package_payload is None:
|
|
220
|
+
return
|
|
221
|
+
existing_package = store.get_artifact("package", "result", {}) or {}
|
|
222
|
+
existing_tag_id = store.get_artifact("package", "tag_id")
|
|
223
|
+
if isinstance(existing_tag_id, int) and existing_tag_id > 0:
|
|
224
|
+
return
|
|
225
|
+
if isinstance(existing_package, dict) and existing_package.get("tagId"):
|
|
226
|
+
return
|
|
227
|
+
result = self.package_tools.package_create(profile=profile, payload=compiled.package_payload)
|
|
228
|
+
package_result = result.get("result") or {}
|
|
229
|
+
tag_id = package_result.get("tagId") or package_result.get("id") or package_result.get("tag_id")
|
|
230
|
+
store.set_artifact("package", "result", result)
|
|
231
|
+
if tag_id is not None:
|
|
232
|
+
store.set_artifact("package", "tag_id", tag_id)
|
|
233
|
+
|
|
234
|
+
def _create_role(self, profile: str, role: CompiledRole, store: RunArtifactStore) -> None:
|
|
235
|
+
existing = store.get_artifact("roles", role.role_id, {}) or {}
|
|
236
|
+
existing_role_id = existing.get("role_id")
|
|
237
|
+
if existing_role_id:
|
|
238
|
+
return
|
|
239
|
+
page = self.role_tools.role_search(profile=profile, keyword=role.name, page_num=1, page_size=50).get("page") or {}
|
|
240
|
+
role_list = page.get("list") if isinstance(page, dict) else []
|
|
241
|
+
matched_role = next(
|
|
242
|
+
(
|
|
243
|
+
item
|
|
244
|
+
for item in (role_list or [])
|
|
245
|
+
if isinstance(item, dict) and item.get("roleName") == role.name and item.get("roleId")
|
|
246
|
+
),
|
|
247
|
+
None,
|
|
248
|
+
)
|
|
249
|
+
if matched_role is None:
|
|
250
|
+
payload = deepcopy(role.payload)
|
|
251
|
+
if not payload.get("users"):
|
|
252
|
+
session_profile = self.role_tools.sessions.get_profile(profile)
|
|
253
|
+
if session_profile is not None:
|
|
254
|
+
payload["users"] = [session_profile.uid]
|
|
255
|
+
result = self.role_tools.role_create(profile=profile, payload=payload)
|
|
256
|
+
role_result = result.get("result") or {}
|
|
257
|
+
role_id = role_result.get("roleId") or role_result.get("id")
|
|
258
|
+
store.set_artifact(
|
|
259
|
+
"roles",
|
|
260
|
+
role.role_id,
|
|
261
|
+
{"role_id": role_id, "result": result, "role_name": role.name, "role_icon": payload.get("roleIcon")},
|
|
262
|
+
)
|
|
263
|
+
return
|
|
264
|
+
store.set_artifact(
|
|
265
|
+
"roles",
|
|
266
|
+
role.role_id,
|
|
267
|
+
{
|
|
268
|
+
"role_id": matched_role.get("roleId"),
|
|
269
|
+
"result": {"result": matched_role},
|
|
270
|
+
"role_name": role.name,
|
|
271
|
+
"role_icon": matched_role.get("roleIcon"),
|
|
272
|
+
"reused": True,
|
|
273
|
+
},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def _create_app(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
277
|
+
existing_artifact = store.get_artifact("apps", entity.entity_id, {}) or {}
|
|
278
|
+
existing_app_key = existing_artifact.get("app_key")
|
|
279
|
+
if isinstance(existing_app_key, str) and existing_app_key:
|
|
280
|
+
next_artifact = deepcopy(existing_artifact)
|
|
281
|
+
next_artifact["reused"] = True
|
|
282
|
+
next_artifact.setdefault("target_mode", "update")
|
|
283
|
+
store.set_artifact("apps", entity.entity_id, next_artifact)
|
|
284
|
+
return
|
|
285
|
+
payload = self._resolve_app_payload(entity.app_create_payload, store)
|
|
286
|
+
result = self.app_tools.app_create(profile=profile, payload=payload)
|
|
287
|
+
app_result = result.get("result") or {}
|
|
288
|
+
app_key = None
|
|
289
|
+
if isinstance(app_result, dict):
|
|
290
|
+
app_key = app_result.get("appKey")
|
|
291
|
+
if app_key is None and isinstance(app_result.get("appKeys"), list) and app_result["appKeys"]:
|
|
292
|
+
app_key = app_result["appKeys"][0]
|
|
293
|
+
app_artifact = {"app_key": app_key, "result": result}
|
|
294
|
+
store.set_artifact("apps", entity.entity_id, app_artifact)
|
|
295
|
+
if app_key:
|
|
296
|
+
try:
|
|
297
|
+
version_result = self.app_tools.app_get_edit_version_no(profile=profile, app_key=app_key).get("result") or {}
|
|
298
|
+
edit_version_no = version_result.get("editVersionNo") or version_result.get("versionNo")
|
|
299
|
+
if edit_version_no is not None:
|
|
300
|
+
app_artifact["edit_version_no"] = int(edit_version_no)
|
|
301
|
+
store.set_artifact("apps", entity.entity_id, app_artifact)
|
|
302
|
+
except Exception:
|
|
303
|
+
store.set_artifact("apps", entity.entity_id, app_artifact)
|
|
304
|
+
raise
|
|
305
|
+
|
|
306
|
+
def _attach_app_to_package(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
307
|
+
tag_id = store.get_artifact("package", "tag_id")
|
|
308
|
+
if not isinstance(tag_id, int) or tag_id <= 0:
|
|
309
|
+
return
|
|
310
|
+
app_artifact = store.get_artifact("apps", entity.entity_id, {}) or {}
|
|
311
|
+
app_key = app_artifact.get("app_key")
|
|
312
|
+
if not isinstance(app_key, str) or not app_key:
|
|
313
|
+
raise ValueError(f"missing app_key for package attach on entity '{entity.entity_id}'")
|
|
314
|
+
|
|
315
|
+
package_detail = self.package_tools.package_get(profile=profile, tag_id=tag_id, include_raw=True)
|
|
316
|
+
package_result = package_detail.get("result") if isinstance(package_detail.get("result"), dict) else {}
|
|
317
|
+
tag_items = [deepcopy(item) for item in package_result.get("tagItems", []) if isinstance(item, dict)]
|
|
318
|
+
if any(_package_item_app_key(item) == app_key for item in tag_items):
|
|
319
|
+
self._record_package_attachment(store, entity.entity_id, app_artifact, tag_id=tag_id, attached=True, reused=True)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
item = {
|
|
323
|
+
"itemType": PACKAGE_ITEM_TYPE_FORM,
|
|
324
|
+
"appKey": app_key,
|
|
325
|
+
"title": entity.display_name,
|
|
326
|
+
"iconUrl": entity.app_create_payload.get("appIcon"),
|
|
327
|
+
}
|
|
328
|
+
insert_at = _resolve_package_item_insert_index(tag_items, entity.app_create_payload.get("ordinal"))
|
|
329
|
+
updated_items = list(tag_items)
|
|
330
|
+
updated_items.insert(insert_at, item)
|
|
331
|
+
self.package_tools.package_sort_items(profile=profile, tag_id=tag_id, tag_items=updated_items)
|
|
332
|
+
|
|
333
|
+
verified_detail = _verify_package_attachment(self.package_tools, profile=profile, tag_id=tag_id, app_key=app_key)
|
|
334
|
+
verified_result = verified_detail.get("result") if isinstance(verified_detail.get("result"), dict) else {}
|
|
335
|
+
verified_items = [deepcopy(existing) for existing in verified_result.get("tagItems", []) if isinstance(existing, dict)]
|
|
336
|
+
if not any(_package_item_app_key(existing) == app_key for existing in verified_items):
|
|
337
|
+
raise QingflowApiError(
|
|
338
|
+
category="runtime",
|
|
339
|
+
message=f"failed to attach app '{app_key}' to package '{tag_id}'",
|
|
340
|
+
details={"tag_id": tag_id, "app_key": app_key},
|
|
341
|
+
)
|
|
342
|
+
store.set_artifact("package", "result", verified_detail)
|
|
343
|
+
store.set_artifact("package", "tag_id", tag_id)
|
|
344
|
+
self._record_package_attachment(store, entity.entity_id, app_artifact, tag_id=tag_id, attached=True, reused=False)
|
|
345
|
+
|
|
346
|
+
def _record_package_attachment(
|
|
347
|
+
self,
|
|
348
|
+
store: RunArtifactStore,
|
|
349
|
+
entity_id: str,
|
|
350
|
+
app_artifact: dict[str, Any],
|
|
351
|
+
*,
|
|
352
|
+
tag_id: int,
|
|
353
|
+
attached: bool,
|
|
354
|
+
reused: bool,
|
|
355
|
+
) -> None:
|
|
356
|
+
next_artifact = deepcopy(app_artifact)
|
|
357
|
+
next_artifact["package_attachment"] = {
|
|
358
|
+
"tag_id": tag_id,
|
|
359
|
+
"attached": attached,
|
|
360
|
+
"reused": reused,
|
|
361
|
+
}
|
|
362
|
+
store.set_artifact("apps", entity_id, next_artifact)
|
|
363
|
+
|
|
364
|
+
def _update_form_base(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
365
|
+
app_key = self._get_app_key(store, entity.entity_id)
|
|
366
|
+
payload = deepcopy(entity.form_base_payload)
|
|
367
|
+
edit_version_no = self._ensure_edit_version(profile, entity.entity_id, store, app_key=app_key)
|
|
368
|
+
if edit_version_no is not None:
|
|
369
|
+
payload["editVersionNo"] = int(edit_version_no)
|
|
370
|
+
try:
|
|
371
|
+
self.app_tools.app_update_form_schema(profile=profile, app_key=app_key, payload=payload)
|
|
372
|
+
except Exception as exc: # noqa: BLE001
|
|
373
|
+
raise self._form_stage_error(
|
|
374
|
+
exc,
|
|
375
|
+
stage_name="form.base",
|
|
376
|
+
entity=entity,
|
|
377
|
+
app_key=app_key,
|
|
378
|
+
payload=payload,
|
|
379
|
+
) from exc
|
|
380
|
+
self._refresh_field_map(profile, entity, store)
|
|
381
|
+
|
|
382
|
+
def _update_form_relations(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
383
|
+
if entity.form_relation_payload is None:
|
|
384
|
+
return
|
|
385
|
+
app_key = self._get_app_key(store, entity.entity_id)
|
|
386
|
+
payload = self._resolve_relation_payload(entity, store)
|
|
387
|
+
edit_version_no = self._ensure_edit_version(profile, entity.entity_id, store, app_key=app_key)
|
|
388
|
+
if edit_version_no is not None:
|
|
389
|
+
payload["editVersionNo"] = int(edit_version_no)
|
|
390
|
+
try:
|
|
391
|
+
self.app_tools.app_update_form_schema(profile=profile, app_key=app_key, payload=payload)
|
|
392
|
+
except Exception as exc: # noqa: BLE001
|
|
393
|
+
raise self._form_stage_error(
|
|
394
|
+
exc,
|
|
395
|
+
stage_name="form.relations",
|
|
396
|
+
entity=entity,
|
|
397
|
+
app_key=app_key,
|
|
398
|
+
payload=payload,
|
|
399
|
+
) from exc
|
|
400
|
+
self._refresh_field_map(profile, entity, store)
|
|
401
|
+
|
|
402
|
+
def _build_workflow(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
403
|
+
if entity.workflow_plan is None:
|
|
404
|
+
return
|
|
405
|
+
app_key = self._get_app_key(store, entity.entity_id)
|
|
406
|
+
workflow_edit_version_no = self._ensure_edit_version(
|
|
407
|
+
profile,
|
|
408
|
+
entity.entity_id,
|
|
409
|
+
store,
|
|
410
|
+
app_key=app_key,
|
|
411
|
+
force_new=True,
|
|
412
|
+
)
|
|
413
|
+
node_artifacts = store.get_artifact("apps", entity.entity_id, {}).get("workflow_nodes", {})
|
|
414
|
+
existing_nodes = self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
|
|
415
|
+
current_nodes = _coerce_workflow_nodes(existing_nodes)
|
|
416
|
+
existing_nodes_by_name = {
|
|
417
|
+
node.get("auditNodeName"): int(node_id)
|
|
418
|
+
for node_id, node in current_nodes.items()
|
|
419
|
+
if isinstance(node, dict) and node.get("auditNodeName")
|
|
420
|
+
}
|
|
421
|
+
applicant_node_id = next(
|
|
422
|
+
(
|
|
423
|
+
int(node_id)
|
|
424
|
+
for node_id, node in current_nodes.items()
|
|
425
|
+
if isinstance(node, dict) and node.get("type") == 0 and node.get("dealType") == 3
|
|
426
|
+
),
|
|
427
|
+
None,
|
|
428
|
+
)
|
|
429
|
+
if applicant_node_id is not None:
|
|
430
|
+
node_artifacts.setdefault("__applicant__", applicant_node_id)
|
|
431
|
+
|
|
432
|
+
desired_global_settings = deepcopy(entity.workflow_plan["global_settings"])
|
|
433
|
+
explicit_global_settings = _has_explicit_workflow_global_settings(desired_global_settings)
|
|
434
|
+
current_global_settings: dict[str, Any] = {}
|
|
435
|
+
if explicit_global_settings:
|
|
436
|
+
current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
|
|
437
|
+
else:
|
|
438
|
+
try:
|
|
439
|
+
current_global_settings = self.workflow_tools.workflow_get_global_settings(profile=profile, app_key=app_key).get("result") or {}
|
|
440
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
441
|
+
api_error = QingflowApiError(**_coerce_nested_error_payload(error))
|
|
442
|
+
if api_error.http_status != 404:
|
|
443
|
+
raise
|
|
444
|
+
current_global_settings = {}
|
|
445
|
+
if explicit_global_settings:
|
|
446
|
+
global_settings = deepcopy(current_global_settings if isinstance(current_global_settings, dict) else {})
|
|
447
|
+
global_settings.update(desired_global_settings)
|
|
448
|
+
global_settings["editVersionNo"] = workflow_edit_version_no or global_settings.get("editVersionNo") or 1
|
|
449
|
+
self.workflow_tools.workflow_update_global_settings(profile=profile, app_key=app_key, payload=global_settings)
|
|
450
|
+
for action in entity.workflow_plan["actions"]:
|
|
451
|
+
if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is not None:
|
|
452
|
+
continue
|
|
453
|
+
if action["action"] == "add_node":
|
|
454
|
+
if action.get("node_type") == "branch":
|
|
455
|
+
existing_branch_id = node_artifacts.get(action["node_id"])
|
|
456
|
+
if existing_branch_id is not None and not _workflow_node_is_branch(current_nodes, existing_branch_id):
|
|
457
|
+
existing_branch_id = None
|
|
458
|
+
if existing_branch_id is not None:
|
|
459
|
+
for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, existing_branch_id), start=1):
|
|
460
|
+
node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
|
|
461
|
+
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
462
|
+
apps_artifact["workflow_nodes"] = node_artifacts
|
|
463
|
+
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
464
|
+
continue
|
|
465
|
+
existing_node_id = node_artifacts.get(action["node_id"]) or existing_nodes_by_name.get(action.get("node_name"))
|
|
466
|
+
if existing_node_id is not None:
|
|
467
|
+
node_artifacts[action["node_id"]] = existing_node_id
|
|
468
|
+
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
469
|
+
apps_artifact["workflow_nodes"] = node_artifacts
|
|
470
|
+
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
471
|
+
continue
|
|
472
|
+
before_node_ids = set(current_nodes)
|
|
473
|
+
payload = self._resolve_workflow_payload(action["payload"], node_artifacts)
|
|
474
|
+
if workflow_edit_version_no is not None:
|
|
475
|
+
payload["editVersionNo"] = int(workflow_edit_version_no)
|
|
476
|
+
if action["action"] == "create_sub_branch":
|
|
477
|
+
result = self.workflow_tools.workflow_create_sub_branch(profile=profile, app_key=app_key, payload=payload)
|
|
478
|
+
elif action["action"] == "update_node":
|
|
479
|
+
target_node_id = node_artifacts.get(action["node_id"])
|
|
480
|
+
if target_node_id is None:
|
|
481
|
+
raise RuntimeError(f"workflow lane '{action['node_id']}' could not be resolved before update")
|
|
482
|
+
result = self.workflow_tools.workflow_update_node(
|
|
483
|
+
profile=profile,
|
|
484
|
+
app_key=app_key,
|
|
485
|
+
audit_node_id=target_node_id,
|
|
486
|
+
payload=payload,
|
|
487
|
+
)
|
|
488
|
+
else:
|
|
489
|
+
result = self.workflow_tools.workflow_add_node(profile=profile, app_key=app_key, payload=payload)
|
|
490
|
+
expected_type = 1 if action.get("node_type") == "branch" else None
|
|
491
|
+
audit_node_id = _extract_workflow_node_id(result.get("result"), expected_type=expected_type)
|
|
492
|
+
if action.get("node_type") == "branch" or action["action"] == "create_sub_branch":
|
|
493
|
+
current_nodes = _coerce_workflow_nodes(
|
|
494
|
+
self.workflow_tools.workflow_list_nodes(profile=profile, app_key=app_key).get("result") or {}
|
|
495
|
+
)
|
|
496
|
+
if audit_node_id is not None:
|
|
497
|
+
node_artifacts[action["node_id"]] = audit_node_id
|
|
498
|
+
if action.get("node_type") == "branch":
|
|
499
|
+
branch_node_id = node_artifacts.get(action["node_id"]) or _find_created_branch_node_id(
|
|
500
|
+
current_nodes,
|
|
501
|
+
before_node_ids=before_node_ids,
|
|
502
|
+
prev_id=payload.get("prevId"),
|
|
503
|
+
)
|
|
504
|
+
if branch_node_id is not None:
|
|
505
|
+
node_artifacts[action["node_id"]] = branch_node_id
|
|
506
|
+
for branch_index, lane_id in enumerate(_find_branch_lane_ids(current_nodes, branch_node_id), start=1):
|
|
507
|
+
node_artifacts[_branch_lane_ref(action["node_id"], branch_index)] = lane_id
|
|
508
|
+
if action["action"] == "create_sub_branch" and node_artifacts.get(action["node_id"]) is None:
|
|
509
|
+
created_lane_id = audit_node_id or _find_created_sub_branch_lane_id(
|
|
510
|
+
current_nodes,
|
|
511
|
+
before_node_ids=before_node_ids,
|
|
512
|
+
branch_node_id=payload.get("auditNodeId"),
|
|
513
|
+
)
|
|
514
|
+
if created_lane_id is not None:
|
|
515
|
+
node_artifacts[action["node_id"]] = created_lane_id
|
|
516
|
+
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
517
|
+
apps_artifact["workflow_nodes"] = node_artifacts
|
|
518
|
+
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
519
|
+
apps_artifact = store.get_artifact("apps", entity.entity_id, {})
|
|
520
|
+
apps_artifact["workflow_nodes"] = node_artifacts
|
|
521
|
+
store.set_artifact("apps", entity.entity_id, apps_artifact)
|
|
522
|
+
|
|
523
|
+
def _build_views(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
524
|
+
app_key = self._get_app_key(store, entity.entity_id)
|
|
525
|
+
field_meta = store.get_artifact("field_maps", entity.entity_id, {})
|
|
526
|
+
field_map = field_meta.get("by_field_id", {})
|
|
527
|
+
schema = field_meta.get("schema", {})
|
|
528
|
+
created_view_keys: list[str] = []
|
|
529
|
+
for view_plan in entity.view_plans:
|
|
530
|
+
create_payload = self._resolve_view_payload(
|
|
531
|
+
view_plan["create_payload"],
|
|
532
|
+
app_key=app_key,
|
|
533
|
+
field_map=field_map,
|
|
534
|
+
schema=schema,
|
|
535
|
+
group_by_field_id=view_plan["group_by_field_id"],
|
|
536
|
+
)
|
|
537
|
+
result = self.view_tools.view_create(profile=profile, payload=create_payload)
|
|
538
|
+
view_result = result.get("result")
|
|
539
|
+
view_key = ((view_result or {}) if isinstance(view_result, dict) else {}).get("viewgraphKey")
|
|
540
|
+
if not view_key and isinstance(view_result, str):
|
|
541
|
+
view_key = view_result
|
|
542
|
+
if not view_key:
|
|
543
|
+
continue
|
|
544
|
+
created_view_keys.append(view_key)
|
|
545
|
+
store.set_artifact("views", f"{entity.entity_id}:{view_plan['view_id']}", {"viewgraph_key": view_key, "result": result})
|
|
546
|
+
if view_plan["column_widths"]:
|
|
547
|
+
que_width_info_list = [
|
|
548
|
+
{"queId": que_id, "queWidth": width}
|
|
549
|
+
for field_id, width in view_plan["column_widths"].items()
|
|
550
|
+
if (que_id := field_map.get(field_id)) is not None
|
|
551
|
+
]
|
|
552
|
+
if que_width_info_list:
|
|
553
|
+
self.view_tools.view_set_column_width(
|
|
554
|
+
profile=profile,
|
|
555
|
+
app_key=app_key,
|
|
556
|
+
payload={
|
|
557
|
+
"viewgraphKey": view_key,
|
|
558
|
+
"beingCustomViewgraph": True,
|
|
559
|
+
"beingReferenceQueWidth": False,
|
|
560
|
+
"queWidthInfoList": que_width_info_list,
|
|
561
|
+
},
|
|
562
|
+
)
|
|
563
|
+
if view_plan["member_config"]:
|
|
564
|
+
self.view_tools.view_update_member_config(profile=profile, viewgraph_key=view_key, payload=view_plan["member_config"])
|
|
565
|
+
if view_plan["apply_config"]:
|
|
566
|
+
self.view_tools.view_update_apply_config(profile=profile, viewgraph_key=view_key, payload=view_plan["apply_config"])
|
|
567
|
+
if view_plan["type"] == "board" and view_plan["group_by_field_id"] and view_plan["config"]:
|
|
568
|
+
que_id = field_map.get(view_plan["group_by_field_id"])
|
|
569
|
+
payload = deepcopy(view_plan["config"])
|
|
570
|
+
if que_id is not None and "queId" not in payload:
|
|
571
|
+
payload["queId"] = que_id
|
|
572
|
+
self.view_tools.view_board_set_lane_config(profile=profile, viewgraph_key=view_key, payload=payload)
|
|
573
|
+
if view_plan["type"] == "gantt":
|
|
574
|
+
if "being_auto_calibration" in view_plan["config"]:
|
|
575
|
+
self.view_tools.view_gantt_switch_auto_calibration(
|
|
576
|
+
profile=profile,
|
|
577
|
+
viewgraph_key=view_key,
|
|
578
|
+
payload={"userAutoCalibration": view_plan["config"]["being_auto_calibration"]},
|
|
579
|
+
)
|
|
580
|
+
if created_view_keys:
|
|
581
|
+
self.view_tools.view_reorder(profile=profile, app_key=app_key, payload=self._resolve_view_reorder_payload(profile, app_key, created_view_keys))
|
|
582
|
+
|
|
583
|
+
def _build_charts(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
584
|
+
app_key = self._get_app_key(store, entity.entity_id)
|
|
585
|
+
field_map = self._get_field_map(store, entity.entity_id)
|
|
586
|
+
bi_field_map = store.get_artifact("field_maps", entity.entity_id, {}).get("bi_by_field_id", {})
|
|
587
|
+
qingbi_fields = self.chart_tools.qingbi_report_list_fields(profile=profile, app_key=app_key).get("items") or []
|
|
588
|
+
qingbi_fields_by_id = {
|
|
589
|
+
field.get("fieldId"): field
|
|
590
|
+
for field in qingbi_fields
|
|
591
|
+
if isinstance(field, dict) and field.get("fieldId")
|
|
592
|
+
}
|
|
593
|
+
existing_charts = self.chart_tools.qingbi_report_list(profile=profile, app_key=app_key).get("items") or []
|
|
594
|
+
existing_chart_keys_by_name = {
|
|
595
|
+
chart.get("chartName"): chart.get("chartId") or chart.get("chartKey")
|
|
596
|
+
for chart in existing_charts
|
|
597
|
+
if isinstance(chart, dict) and chart.get("chartName") and (chart.get("chartId") or chart.get("chartKey"))
|
|
598
|
+
}
|
|
599
|
+
created_chart_keys: list[str] = []
|
|
600
|
+
for chart_plan in entity.chart_plans:
|
|
601
|
+
chart_artifact_key = f"{entity.entity_id}:{chart_plan['chart_id']}"
|
|
602
|
+
chart_artifact = store.get_artifact("charts", chart_artifact_key, {}) or {}
|
|
603
|
+
chart_key = (
|
|
604
|
+
chart_artifact.get("bi_chart_id")
|
|
605
|
+
or chart_artifact.get("chart_key")
|
|
606
|
+
or existing_chart_keys_by_name.get(chart_plan["name"])
|
|
607
|
+
)
|
|
608
|
+
result = chart_artifact.get("result")
|
|
609
|
+
if not chart_key:
|
|
610
|
+
create_payload = deepcopy(chart_plan["create_payload"])
|
|
611
|
+
create_payload["dataSourceId"] = app_key
|
|
612
|
+
create_payload["chartId"] = create_payload.get("chartId") if create_payload.get("chartId") != "__BI_CHART_ID__" else f"mcp_{uuid4().hex[:16]}"
|
|
613
|
+
result = self.chart_tools.qingbi_report_create(profile=profile, payload=create_payload)
|
|
614
|
+
chart_key = ((result.get("result") or {}) if isinstance(result.get("result"), dict) else {}).get("chartId")
|
|
615
|
+
if not chart_key:
|
|
616
|
+
continue
|
|
617
|
+
created_chart_keys.append(chart_key)
|
|
618
|
+
config_payload = self._resolve_qingbi_chart_config_payload(
|
|
619
|
+
chart_plan["config_payload"],
|
|
620
|
+
entity=entity,
|
|
621
|
+
field_map=field_map,
|
|
622
|
+
bi_field_map=bi_field_map,
|
|
623
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
624
|
+
app_key=app_key,
|
|
625
|
+
)
|
|
626
|
+
self.chart_tools.qingbi_report_update_config(profile=profile, chart_id=chart_key, payload=config_payload)
|
|
627
|
+
chart_info = {
|
|
628
|
+
"chartName": chart_plan["name"],
|
|
629
|
+
"chartType": chart_plan["chart_type"],
|
|
630
|
+
"dataSourceId": app_key,
|
|
631
|
+
"dataSourceType": "qingflow",
|
|
632
|
+
}
|
|
633
|
+
chart_info.update(config_payload)
|
|
634
|
+
chart_info.setdefault("chartName", chart_plan["name"])
|
|
635
|
+
chart_info["biChartId"] = chart_key
|
|
636
|
+
store.set_artifact(
|
|
637
|
+
"charts",
|
|
638
|
+
chart_artifact_key,
|
|
639
|
+
{
|
|
640
|
+
"bi_chart_id": chart_key,
|
|
641
|
+
"chart_key": chart_key,
|
|
642
|
+
"result": result,
|
|
643
|
+
"chart_info": chart_info,
|
|
644
|
+
},
|
|
645
|
+
)
|
|
646
|
+
if created_chart_keys:
|
|
647
|
+
self.chart_tools.qingbi_report_reorder(profile=profile, app_key=app_key, chart_ids=created_chart_keys)
|
|
648
|
+
|
|
649
|
+
def _build_portal(self, profile: str, compiled: CompiledSolution, store: RunArtifactStore) -> None:
|
|
650
|
+
if compiled.portal_plan is None:
|
|
651
|
+
return
|
|
652
|
+
if _portal_plan_has_source_type(compiled.portal_plan, "grid"):
|
|
653
|
+
self.workspace_tools.workspace_set_plugin_status(
|
|
654
|
+
profile=profile,
|
|
655
|
+
plugin_id=GRID_COMPONENT_PLUGIN_ID,
|
|
656
|
+
being_installed=True,
|
|
657
|
+
)
|
|
658
|
+
store.set_artifact(
|
|
659
|
+
"portal",
|
|
660
|
+
"grid_plugin_install",
|
|
661
|
+
{
|
|
662
|
+
"plugin_id": GRID_COMPONENT_PLUGIN_ID,
|
|
663
|
+
"being_installed": True,
|
|
664
|
+
},
|
|
665
|
+
)
|
|
666
|
+
portal_artifact = store.get_artifact("portal", "result", {}) or {}
|
|
667
|
+
dash_key = store.get_artifact("portal", "dash_key")
|
|
668
|
+
result = portal_artifact
|
|
669
|
+
if not dash_key:
|
|
670
|
+
create_payload = self._resolve_portal_payload(compiled.portal_plan["create_payload"], store, base_payload=None)
|
|
671
|
+
result = self.portal_tools.portal_create(profile=profile, payload=create_payload)
|
|
672
|
+
portal_result = result.get("result") or {}
|
|
673
|
+
dash_key = portal_result.get("dashKey")
|
|
674
|
+
store.set_artifact("portal", "result", result)
|
|
675
|
+
if dash_key:
|
|
676
|
+
store.set_artifact("portal", "dash_key", dash_key)
|
|
677
|
+
if dash_key:
|
|
678
|
+
base_payload = self.portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
|
|
679
|
+
update_payload = self._resolve_portal_payload(compiled.portal_plan["update_payload"], store, base_payload=base_payload)
|
|
680
|
+
self.portal_tools.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
681
|
+
self._refresh_portal_artifact(profile=profile, store=store, being_draft=True, artifact_key="draft_result")
|
|
682
|
+
|
|
683
|
+
def _build_navigation(self, profile: str, compiled: CompiledSolution, store: RunArtifactStore) -> None:
|
|
684
|
+
try:
|
|
685
|
+
created_items = self._create_navigation_items(profile, compiled, store)
|
|
686
|
+
except Exception as exc: # noqa: BLE001
|
|
687
|
+
api_error = _coerce_qingflow_error(exc)
|
|
688
|
+
if api_error is None or not _is_navigation_plugin_unavailable(api_error):
|
|
689
|
+
raise
|
|
690
|
+
self.workspace_tools.workspace_set_plugin_status(
|
|
691
|
+
profile=profile,
|
|
692
|
+
plugin_id=NAVIGATION_PLUGIN_ID,
|
|
693
|
+
being_installed=True,
|
|
694
|
+
)
|
|
695
|
+
created_items = self._create_navigation_items(profile, compiled, store)
|
|
696
|
+
store.set_artifact(
|
|
697
|
+
"navigation",
|
|
698
|
+
"plugin_install",
|
|
699
|
+
{
|
|
700
|
+
"plugin_id": NAVIGATION_PLUGIN_ID,
|
|
701
|
+
"being_installed": True,
|
|
702
|
+
},
|
|
703
|
+
)
|
|
704
|
+
store.set_artifact("navigation", "items", created_items)
|
|
705
|
+
|
|
706
|
+
def _refresh_portal_artifact(
|
|
707
|
+
self,
|
|
708
|
+
*,
|
|
709
|
+
profile: str,
|
|
710
|
+
store: RunArtifactStore,
|
|
711
|
+
being_draft: bool,
|
|
712
|
+
artifact_key: str,
|
|
713
|
+
) -> None:
|
|
714
|
+
dash_key = store.get_artifact("portal", "dash_key")
|
|
715
|
+
if not dash_key:
|
|
716
|
+
return
|
|
717
|
+
try:
|
|
718
|
+
result = self.portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
|
|
719
|
+
except Exception: # noqa: BLE001
|
|
720
|
+
return
|
|
721
|
+
store.set_artifact("portal", artifact_key, result)
|
|
722
|
+
store.set_artifact("portal", "result", result)
|
|
723
|
+
|
|
724
|
+
def _create_navigation_items(
|
|
725
|
+
self,
|
|
726
|
+
profile: str,
|
|
727
|
+
compiled: CompiledSolution,
|
|
728
|
+
store: RunArtifactStore,
|
|
729
|
+
) -> list[dict[str, Any]]:
|
|
730
|
+
created_items: list[dict[str, Any]] = []
|
|
731
|
+
for ordinal, item in enumerate(compiled.navigation_plan, start=1):
|
|
732
|
+
result = self.navigation_tools.navigation_create(
|
|
733
|
+
profile=profile,
|
|
734
|
+
payload=self._resolve_navigation_payload(item["payload"], store, ordinal),
|
|
735
|
+
)
|
|
736
|
+
created = {"item_id": item["item_id"], "result": result}
|
|
737
|
+
created_items.append(created)
|
|
738
|
+
for child_ordinal, child in enumerate(item["children"], start=1):
|
|
739
|
+
child_payload = self._resolve_navigation_payload(child["payload"], store, child_ordinal)
|
|
740
|
+
child_payload["parentNavigationItemId"] = ((result.get("result") or {}) if isinstance(result.get("result"), dict) else {}).get("navigationItemId")
|
|
741
|
+
created_items.append(
|
|
742
|
+
{
|
|
743
|
+
"item_id": child["item_id"],
|
|
744
|
+
"result": self.navigation_tools.navigation_create(profile=profile, payload=child_payload),
|
|
745
|
+
}
|
|
746
|
+
)
|
|
747
|
+
return created_items
|
|
748
|
+
|
|
749
|
+
def _refresh_field_map(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
750
|
+
app_key = self._get_app_key(store, entity.entity_id)
|
|
751
|
+
schema = self.app_tools.app_get_form_schema(
|
|
752
|
+
profile=profile,
|
|
753
|
+
app_key=app_key,
|
|
754
|
+
form_type=1,
|
|
755
|
+
being_draft=True,
|
|
756
|
+
being_apply=None,
|
|
757
|
+
audit_node_id=None,
|
|
758
|
+
include_raw=True,
|
|
759
|
+
)
|
|
760
|
+
result = schema.get("result") or {}
|
|
761
|
+
field_map = extract_field_map(result)
|
|
762
|
+
label_to_field_id = {label: field_id for field_id, label in entity.field_labels.items()}
|
|
763
|
+
mapped = {field_id: field_map[label] for label, field_id in label_to_field_id.items() if label in field_map}
|
|
764
|
+
current_edit_version_no = self._get_edit_version_no(store, entity.entity_id)
|
|
765
|
+
response_edit_version_no = result.get("editVersionNo")
|
|
766
|
+
store.set_artifact(
|
|
767
|
+
"field_maps",
|
|
768
|
+
entity.entity_id,
|
|
769
|
+
{
|
|
770
|
+
"by_field_id": mapped,
|
|
771
|
+
"bi_by_field_id": {field_id: f"{app_key}:{que_id}" for field_id, que_id in mapped.items()},
|
|
772
|
+
"by_label": field_map,
|
|
773
|
+
"edit_version_no": int(response_edit_version_no or current_edit_version_no or 1),
|
|
774
|
+
"schema": result,
|
|
775
|
+
},
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
def _resolve_app_payload(self, payload: dict[str, Any], store: RunArtifactStore) -> dict[str, Any]:
|
|
779
|
+
data = deepcopy(payload)
|
|
780
|
+
if "tagIds" in data:
|
|
781
|
+
tag_id = store.get_artifact("package", "tag_id")
|
|
782
|
+
data["tagIds"] = [tag_id] if tag_id is not None else []
|
|
783
|
+
return data
|
|
784
|
+
|
|
785
|
+
def _resolve_relation_payload(self, entity: CompiledEntity, store: RunArtifactStore) -> dict[str, Any]:
|
|
786
|
+
payload = deepcopy(entity.form_relation_payload or {})
|
|
787
|
+
for line in payload.get("formQues", []):
|
|
788
|
+
self._resolve_reference_questions(line, entity, store)
|
|
789
|
+
payload["questionRelations"] = []
|
|
790
|
+
return payload
|
|
791
|
+
|
|
792
|
+
def _resolve_reference_questions(self, questions: list[dict[str, Any]], entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
793
|
+
for question in questions:
|
|
794
|
+
if question.get("queType") == 24:
|
|
795
|
+
for inner_row in question.get("innerQuestions", []):
|
|
796
|
+
self._resolve_reference_questions(inner_row, entity, store)
|
|
797
|
+
continue
|
|
798
|
+
if question.get("queType") != 25:
|
|
799
|
+
continue
|
|
800
|
+
target_entity_id = self._target_entity_id_from_label(entity, question["queTitle"])
|
|
801
|
+
if not target_entity_id:
|
|
802
|
+
continue
|
|
803
|
+
target_field_label = self._target_field_label(entity, question["queTitle"], store)
|
|
804
|
+
target_field_id = self._target_field_id_from_label(entity, question["queTitle"])
|
|
805
|
+
target_meta = store.get_artifact("field_maps", target_entity_id, {})
|
|
806
|
+
target_app = store.get_artifact("apps", target_entity_id, {})
|
|
807
|
+
target_label_map = target_meta.get("by_label", {})
|
|
808
|
+
target_que_id = target_label_map.get(target_field_label, 0)
|
|
809
|
+
reference_config = deepcopy(question.get("referenceConfig") or {})
|
|
810
|
+
reference_config["referAppKey"] = target_app.get("app_key")
|
|
811
|
+
reference_config["referQueId"] = target_que_id
|
|
812
|
+
for ordinal, refer_question in enumerate(reference_config.get("referQuestions", []), start=1):
|
|
813
|
+
refer_field_id = target_field_id if ordinal == 1 else refer_question.get("_field_id") or target_field_id
|
|
814
|
+
refer_label = self._field_label_from_id(target_entity_id, refer_field_id, store)
|
|
815
|
+
refer_que_id = target_meta.get("by_field_id", {}).get(refer_field_id) or target_label_map.get(refer_label, 0)
|
|
816
|
+
refer_field_spec = self._field_spec_from_store(store, target_entity_id, refer_field_id)
|
|
817
|
+
refer_field_type = refer_field_spec.get("type")
|
|
818
|
+
refer_question["queId"] = refer_que_id
|
|
819
|
+
refer_question["queTitle"] = refer_label
|
|
820
|
+
if refer_field_type:
|
|
821
|
+
refer_question["queType"] = str(QUESTION_TYPE_MAP[FieldType(refer_field_type)])
|
|
822
|
+
refer_question["ordinal"] = ordinal
|
|
823
|
+
refer_question.pop("_field_id", None)
|
|
824
|
+
auth_ques = []
|
|
825
|
+
for auth_que in reference_config.get("referAuthQues", []):
|
|
826
|
+
resolved = deepcopy(auth_que)
|
|
827
|
+
refer_field_id = resolved.pop("_field_id", None) or target_field_id
|
|
828
|
+
refer_que_id = target_meta.get("by_field_id", {}).get(refer_field_id)
|
|
829
|
+
if refer_que_id is None:
|
|
830
|
+
continue
|
|
831
|
+
resolved["queId"] = refer_que_id
|
|
832
|
+
resolved["queAuth"] = int(resolved.get("queAuth", 1))
|
|
833
|
+
auth_ques.append(resolved)
|
|
834
|
+
if not auth_ques:
|
|
835
|
+
fallback_que_id = target_meta.get("by_field_id", {}).get(target_field_id)
|
|
836
|
+
if fallback_que_id is not None:
|
|
837
|
+
auth_ques.append({"queId": fallback_que_id, "queAuth": 1})
|
|
838
|
+
reference_config["referAuthQues"] = auth_ques
|
|
839
|
+
reference_config["fieldNameShow"] = bool(reference_config.get("fieldNameShow", True))
|
|
840
|
+
fill_rules = []
|
|
841
|
+
for fill_rule in reference_config.get("referFillRules", []):
|
|
842
|
+
resolved = deepcopy(fill_rule)
|
|
843
|
+
current_field_id = resolved.pop("field_id", None)
|
|
844
|
+
target_fill_field_id = resolved.pop("target_field_id", None)
|
|
845
|
+
if not current_field_id or not target_fill_field_id:
|
|
846
|
+
continue
|
|
847
|
+
resolved["queId"] = store.get_artifact("field_maps", entity.entity_id, {}).get("by_field_id", {}).get(current_field_id)
|
|
848
|
+
resolved["relatedQueId"] = target_meta.get("by_field_id", {}).get(target_fill_field_id)
|
|
849
|
+
resolved["queTitle"] = self._field_label_from_id(entity.entity_id, current_field_id, store)
|
|
850
|
+
resolved["relatedQueTitle"] = self._field_label_from_id(target_entity_id, target_fill_field_id, store)
|
|
851
|
+
resolved["currentQuoteQueId"] = question.get("queId") or question.get("queTempId")
|
|
852
|
+
if resolved["queId"] and resolved["relatedQueId"]:
|
|
853
|
+
fill_rules.append(resolved)
|
|
854
|
+
reference_config["referFillRules"] = fill_rules
|
|
855
|
+
reference_config.pop("_targetFieldId", None)
|
|
856
|
+
reference_config.pop("_targetEntityId", None)
|
|
857
|
+
question["referenceConfig"] = reference_config
|
|
858
|
+
|
|
859
|
+
def _resolve_workflow_payload(self, payload: dict[str, Any], node_artifacts: dict[str, int]) -> dict[str, Any]:
|
|
860
|
+
data = deepcopy(payload)
|
|
861
|
+
prev_ref = data.pop("prevNodeRef", None)
|
|
862
|
+
if prev_ref:
|
|
863
|
+
data["prevId"] = node_artifacts.get(prev_ref, 0)
|
|
864
|
+
audit_ref = data.pop("auditNodeRef", None)
|
|
865
|
+
if audit_ref:
|
|
866
|
+
data["auditNodeId"] = node_artifacts.get(audit_ref, 0)
|
|
867
|
+
audit_user_infos = data.get("auditUserInfos")
|
|
868
|
+
if isinstance(audit_user_infos, dict):
|
|
869
|
+
role_refs = audit_user_infos.pop("role_refs", [])
|
|
870
|
+
if role_refs:
|
|
871
|
+
roles = []
|
|
872
|
+
for role_ref in role_refs:
|
|
873
|
+
role_artifact = self._current_store.get_artifact("roles", role_ref, {}) if hasattr(self, "_current_store") else {}
|
|
874
|
+
role_id = role_artifact.get("role_id")
|
|
875
|
+
if role_id is None:
|
|
876
|
+
raise QingflowApiError.config_error(f"workflow role '{role_ref}' has not been created")
|
|
877
|
+
roles.append(
|
|
878
|
+
{
|
|
879
|
+
"roleId": role_id,
|
|
880
|
+
"roleName": role_artifact.get("role_name") or role_ref,
|
|
881
|
+
"roleIcon": role_artifact.get("role_icon") or "ex-user-outlined",
|
|
882
|
+
"beingFrontendConfig": True,
|
|
883
|
+
}
|
|
884
|
+
)
|
|
885
|
+
audit_user_infos["role"] = roles
|
|
886
|
+
if data.get("auditUserInfos") is None:
|
|
887
|
+
data.pop("auditUserInfos", None)
|
|
888
|
+
return data
|
|
889
|
+
|
|
890
|
+
def _resolve_view_payload(
|
|
891
|
+
self,
|
|
892
|
+
payload: dict[str, Any],
|
|
893
|
+
*,
|
|
894
|
+
app_key: str,
|
|
895
|
+
field_map: dict[str, int],
|
|
896
|
+
schema: dict[str, Any],
|
|
897
|
+
group_by_field_id: str | None,
|
|
898
|
+
) -> dict[str, Any]:
|
|
899
|
+
data = deepcopy(payload)
|
|
900
|
+
data["appKey"] = app_key
|
|
901
|
+
visible_que_ids = [field_map[field_id] for field_id in data.get("viewgraphQueIds", []) if field_id in field_map]
|
|
902
|
+
data["viewgraphQueIds"] = visible_que_ids
|
|
903
|
+
data.setdefault("defaultRowHigh", "compact")
|
|
904
|
+
data.setdefault("asosChartVisible", False)
|
|
905
|
+
data.setdefault("beingNeedPass", False)
|
|
906
|
+
data.setdefault("beingAuditRecordVisible", True)
|
|
907
|
+
data.setdefault("beingQrobotRecordVisible", False)
|
|
908
|
+
data.setdefault("beingPrintStatus", False)
|
|
909
|
+
data.setdefault("beingDefaultPrintTplStatus", False)
|
|
910
|
+
data.setdefault("beingCommentStatus", False)
|
|
911
|
+
data.setdefault("beingWorkflowNodeFutureListVisible", True)
|
|
912
|
+
data.setdefault("dataPermissionType", "CUSTOM")
|
|
913
|
+
data.setdefault("dataScope", "ALL")
|
|
914
|
+
data.setdefault("needPass", False)
|
|
915
|
+
data.setdefault("viewgraphPass", "")
|
|
916
|
+
data.setdefault("beingImageAdaption", False)
|
|
917
|
+
data.setdefault("clippingMode", "default")
|
|
918
|
+
data.setdefault("frontCoverQueId", None)
|
|
919
|
+
data.setdefault("printTpls", [])
|
|
920
|
+
data.setdefault("usages", [])
|
|
921
|
+
data.setdefault("asosChartConfig", {"limitType": 1, "asosChartIdList": []})
|
|
922
|
+
data.setdefault("viewgraphGanttConfigVO", None)
|
|
923
|
+
data.setdefault("viewgraphHierarchyConfigVO", None)
|
|
924
|
+
data.setdefault("viewgraphLimitFormula", "")
|
|
925
|
+
data.setdefault("buttonConfigDTOList", [])
|
|
926
|
+
if not data.get("viewgraphQuestions"):
|
|
927
|
+
data["viewgraphQuestions"] = _build_viewgraph_questions(schema, visible_que_ids)
|
|
928
|
+
if not data.get("viewgraphSorts"):
|
|
929
|
+
data["viewgraphSorts"] = [{"queId": 0, "beingSortAscend": True, "queType": 8}]
|
|
930
|
+
if data.get("viewgraphType") in {"cardView", "boardView", "ganttView", "hierarchyView"}:
|
|
931
|
+
data["titleQue"] = visible_que_ids[0] if visible_que_ids else None
|
|
932
|
+
else:
|
|
933
|
+
data.setdefault("titleQue", None)
|
|
934
|
+
if data.get("viewgraphType") == "boardView":
|
|
935
|
+
data["groupQueId"] = field_map.get(group_by_field_id) if group_by_field_id else 1
|
|
936
|
+
if data.get("viewgraphType") == "ganttView":
|
|
937
|
+
gantt_payload = self._resolve_gantt_payload(payload, field_map)
|
|
938
|
+
if gantt_payload.get("viewgraphGanttConfigVO"):
|
|
939
|
+
data["viewgraphGanttConfigVO"] = gantt_payload["viewgraphGanttConfigVO"]
|
|
940
|
+
return data
|
|
941
|
+
|
|
942
|
+
def _resolve_gantt_payload(self, config: dict[str, Any], field_map: dict[str, int]) -> dict[str, Any]:
|
|
943
|
+
start_field_id = config.get("start_field_id")
|
|
944
|
+
end_field_id = config.get("end_field_id")
|
|
945
|
+
title_field_id = config.get("title_field_id")
|
|
946
|
+
if not any((start_field_id, end_field_id, title_field_id)):
|
|
947
|
+
return {}
|
|
948
|
+
gantt_config = {
|
|
949
|
+
"titleQueId": field_map.get(title_field_id),
|
|
950
|
+
"startTimeQueId": field_map.get(start_field_id),
|
|
951
|
+
"endTimeQueId": field_map.get(end_field_id),
|
|
952
|
+
"defaultTimeDimension": "week",
|
|
953
|
+
"ganttGroupVOList": [],
|
|
954
|
+
"ganttDependencyVO": {
|
|
955
|
+
"dependencyQueId": None,
|
|
956
|
+
"predecessorTaskQueId": None,
|
|
957
|
+
"startEndOptionId": None,
|
|
958
|
+
"startStartOptionId": None,
|
|
959
|
+
"endEndOptionId": None,
|
|
960
|
+
"endStartOptionId": None,
|
|
961
|
+
},
|
|
962
|
+
"ganttAutoCalibrationVO": {
|
|
963
|
+
"autoCalibrationRuleVO": {
|
|
964
|
+
"startStartBegin": False,
|
|
965
|
+
"startEndBegin": False,
|
|
966
|
+
"startEndFinish": False,
|
|
967
|
+
"endStartBegin": True,
|
|
968
|
+
"endStartFinish": True,
|
|
969
|
+
"endEndFinish": False,
|
|
970
|
+
},
|
|
971
|
+
"beingAutoCalibration": False,
|
|
972
|
+
"userAutoCalibration": False,
|
|
973
|
+
},
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
"viewgraphGanttConfigVO": gantt_config,
|
|
977
|
+
**{key: value for key, value in config.items() if key not in {"start_field_id", "end_field_id", "title_field_id"}},
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
def _resolve_qingbi_chart_config_payload(
|
|
981
|
+
self,
|
|
982
|
+
payload: dict[str, Any],
|
|
983
|
+
*,
|
|
984
|
+
entity: CompiledEntity,
|
|
985
|
+
field_map: dict[str, int],
|
|
986
|
+
bi_field_map: dict[str, str],
|
|
987
|
+
qingbi_fields_by_id: dict[str, dict[str, Any]],
|
|
988
|
+
app_key: str,
|
|
989
|
+
) -> dict[str, Any]:
|
|
990
|
+
data = {
|
|
991
|
+
"chartName": payload.get("chartName"),
|
|
992
|
+
"chartType": payload.get("chartType"),
|
|
993
|
+
"dataSource": {"dataSourceId": app_key, "dataSourceType": "qingflow"},
|
|
994
|
+
"selectedDimensions": self._resolve_qingbi_dimension_fields(
|
|
995
|
+
payload.get("selectedDimensionFieldIds", []),
|
|
996
|
+
entity=entity,
|
|
997
|
+
field_map=field_map,
|
|
998
|
+
bi_field_map=bi_field_map,
|
|
999
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
1000
|
+
),
|
|
1001
|
+
"selectedMetrics": self._resolve_qingbi_metric_fields(
|
|
1002
|
+
payload.get("selectedMetricFieldIds", []),
|
|
1003
|
+
aggregate=str(payload.get("aggregate") or "count").lower(),
|
|
1004
|
+
entity=entity,
|
|
1005
|
+
field_map=field_map,
|
|
1006
|
+
bi_field_map=bi_field_map,
|
|
1007
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
1008
|
+
),
|
|
1009
|
+
"beforeAggregationFilterMatrix": [],
|
|
1010
|
+
"afterAggregationFilterMatrix": [],
|
|
1011
|
+
"chartStyleConfigs": deepcopy(payload.get("chartStyleConfigs", [])),
|
|
1012
|
+
"conditionFormatMatrix": deepcopy(payload.get("conditionFormatMatrix", [])),
|
|
1013
|
+
"displayLimitConfig": deepcopy(payload.get("displayLimitConfig", {"status": 1, "type": "asc", "limit": 20})),
|
|
1014
|
+
"rawDataConfigDTO": deepcopy(payload.get("rawDataConfigDTO", {"beingOpen": False, "authInfo": {"type": "ws", "contactAuth": {"type": "all", "authMembers": {}}, "externalMemberAuth": {"type": "not", "authMembers": {}}}, "fieldInfoList": []})),
|
|
1015
|
+
}
|
|
1016
|
+
for key in (
|
|
1017
|
+
"selectedTime",
|
|
1018
|
+
"xDimensions",
|
|
1019
|
+
"yDimensions",
|
|
1020
|
+
"xMetrics",
|
|
1021
|
+
"yMetrics",
|
|
1022
|
+
"leftMetrics",
|
|
1023
|
+
"rightMetrics",
|
|
1024
|
+
"pieType",
|
|
1025
|
+
"radarType",
|
|
1026
|
+
"queryConditionFieldIds",
|
|
1027
|
+
"queryConditionStatus",
|
|
1028
|
+
"queryConditionExact",
|
|
1029
|
+
):
|
|
1030
|
+
if key in payload:
|
|
1031
|
+
data[key] = deepcopy(payload[key])
|
|
1032
|
+
return data
|
|
1033
|
+
|
|
1034
|
+
def _resolve_graph_payload(self, payload: dict[str, Any], field_map: dict[str, int]) -> dict[str, Any]:
|
|
1035
|
+
data = deepcopy(payload)
|
|
1036
|
+
if "field_id" in data:
|
|
1037
|
+
field_id = data.pop("field_id")
|
|
1038
|
+
que_id = field_map.get(field_id)
|
|
1039
|
+
if que_id is None:
|
|
1040
|
+
return {}
|
|
1041
|
+
data["queId"] = que_id
|
|
1042
|
+
return data
|
|
1043
|
+
|
|
1044
|
+
def _resolve_match_rule_groups(self, groups: list[list[dict[str, Any]]], field_map: dict[str, int]) -> list[list[dict[str, Any]]]:
|
|
1045
|
+
resolved_groups: list[list[dict[str, Any]]] = []
|
|
1046
|
+
for group in groups or []:
|
|
1047
|
+
resolved_group: list[dict[str, Any]] = []
|
|
1048
|
+
for rule in group or []:
|
|
1049
|
+
resolved_rule = deepcopy(rule)
|
|
1050
|
+
field_id = resolved_rule.pop("field_id", None)
|
|
1051
|
+
if field_id is not None:
|
|
1052
|
+
que_id = field_map.get(field_id)
|
|
1053
|
+
if que_id is None:
|
|
1054
|
+
continue
|
|
1055
|
+
resolved_rule["queId"] = que_id
|
|
1056
|
+
resolved_group.append(resolved_rule)
|
|
1057
|
+
if resolved_group:
|
|
1058
|
+
resolved_groups.append(resolved_group)
|
|
1059
|
+
return resolved_groups
|
|
1060
|
+
|
|
1061
|
+
def _resolve_portal_payload(
|
|
1062
|
+
self,
|
|
1063
|
+
payload: dict[str, Any],
|
|
1064
|
+
store: RunArtifactStore,
|
|
1065
|
+
*,
|
|
1066
|
+
base_payload: dict[str, Any] | None = None,
|
|
1067
|
+
) -> dict[str, Any]:
|
|
1068
|
+
data = deepcopy(base_payload) if isinstance(base_payload, dict) else deepcopy(payload)
|
|
1069
|
+
data["dashName"] = payload.get("dashName") or data.get("dashName")
|
|
1070
|
+
data["dashIcon"] = payload.get("dashIcon") or data.get("dashIcon")
|
|
1071
|
+
data["auth"] = deepcopy(payload.get("auth") or data.get("auth"))
|
|
1072
|
+
data["hideCopyright"] = payload.get("hideCopyright", data.get("hideCopyright", False))
|
|
1073
|
+
tag_id = store.get_artifact("package", "tag_id")
|
|
1074
|
+
data["tags"] = deepcopy(payload.get("tags") or data.get("tags") or [])
|
|
1075
|
+
for tag in data.get("tags", []):
|
|
1076
|
+
if tag.get("tagId") == "__PACKAGE_TAG_ID__":
|
|
1077
|
+
tag["tagId"] = tag_id
|
|
1078
|
+
dash_global_config = deepcopy(payload.get("dashGlobalConfig") or data.get("dashGlobalConfig") or {})
|
|
1079
|
+
dash_global_config.pop("layout", None)
|
|
1080
|
+
dash_global_config["interval"] = dash_global_config.get("interval") or 60
|
|
1081
|
+
dash_global_config["beingAutoRefresh"] = bool(dash_global_config.get("beingAutoRefresh", False))
|
|
1082
|
+
data["dashGlobalConfig"] = dash_global_config
|
|
1083
|
+
if "components" in payload:
|
|
1084
|
+
data["components"] = self._build_portal_components(payload.get("components", []), store)
|
|
1085
|
+
return data
|
|
1086
|
+
|
|
1087
|
+
def _resolve_navigation_payload(self, payload: dict[str, Any], store: RunArtifactStore, ordinal: int) -> dict[str, Any]:
|
|
1088
|
+
data = deepcopy(payload)
|
|
1089
|
+
if data.get("tagId") == "__PACKAGE_TAG_ID__":
|
|
1090
|
+
data["tagId"] = store.get_artifact("package", "tag_id")
|
|
1091
|
+
if "appRef" in data:
|
|
1092
|
+
ref = data.pop("appRef")
|
|
1093
|
+
data["appKey"] = self._get_app_key(store, ref["entity_id"])
|
|
1094
|
+
if "chartRef" in data:
|
|
1095
|
+
ref = data.pop("chartRef")
|
|
1096
|
+
chart = store.get_artifact("charts", f"{ref['entity_id']}:{ref['chart_id']}", {})
|
|
1097
|
+
data["chartKey"] = chart.get("chart_key")
|
|
1098
|
+
if data.get("dashKey") == "__PORTAL_DASH_KEY__":
|
|
1099
|
+
data["dashKey"] = store.get_artifact("portal", "dash_key")
|
|
1100
|
+
if "viewRef" in data:
|
|
1101
|
+
ref = data.pop("viewRef")
|
|
1102
|
+
view = store.get_artifact("views", f"{ref['entity_id']}:{ref['view_id']}", {})
|
|
1103
|
+
data["viewgraphKey"] = view.get("viewgraph_key")
|
|
1104
|
+
data["ordinal"] = ordinal
|
|
1105
|
+
return data
|
|
1106
|
+
|
|
1107
|
+
def _resolve_view_reorder_payload(self, profile: str, app_key: str, created_view_keys: list[str]) -> list[dict[str, Any]]:
|
|
1108
|
+
current_view_list = self.view_tools.view_list(profile=profile, app_key=app_key).get("result") or []
|
|
1109
|
+
if not isinstance(current_view_list, list) or not current_view_list:
|
|
1110
|
+
return [{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": created_view_keys}]
|
|
1111
|
+
|
|
1112
|
+
created_key_set = set(created_view_keys)
|
|
1113
|
+
assigned_keys: set[str] = set()
|
|
1114
|
+
payload: list[dict[str, Any]] = []
|
|
1115
|
+
for group in current_view_list:
|
|
1116
|
+
if not isinstance(group, dict):
|
|
1117
|
+
continue
|
|
1118
|
+
ordinal_type = group.get("ordinalType")
|
|
1119
|
+
view_keys = [
|
|
1120
|
+
view.get("viewKey")
|
|
1121
|
+
for view in group.get("viewList", [])
|
|
1122
|
+
if isinstance(view, dict) and view.get("viewKey")
|
|
1123
|
+
]
|
|
1124
|
+
if not ordinal_type or not view_keys:
|
|
1125
|
+
continue
|
|
1126
|
+
prioritized_keys = [key for key in created_view_keys if key in view_keys]
|
|
1127
|
+
if prioritized_keys:
|
|
1128
|
+
assigned_keys.update(prioritized_keys)
|
|
1129
|
+
remaining_keys = [key for key in view_keys if key not in created_key_set]
|
|
1130
|
+
payload.append({"ordinalType": ordinal_type, "viewKeyList": prioritized_keys + remaining_keys})
|
|
1131
|
+
else:
|
|
1132
|
+
payload.append({"ordinalType": ordinal_type, "viewKeyList": view_keys})
|
|
1133
|
+
|
|
1134
|
+
unassigned_keys = [key for key in created_view_keys if key not in assigned_keys]
|
|
1135
|
+
if unassigned_keys:
|
|
1136
|
+
fixed_group = next((group for group in payload if group.get("ordinalType") == "FIXED_VIEW_LIST"), None)
|
|
1137
|
+
if fixed_group is None:
|
|
1138
|
+
payload.insert(0, {"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": unassigned_keys})
|
|
1139
|
+
else:
|
|
1140
|
+
fixed_group["viewKeyList"] = unassigned_keys + [key for key in fixed_group["viewKeyList"] if key not in set(unassigned_keys)]
|
|
1141
|
+
return payload or [{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": created_view_keys}]
|
|
1142
|
+
|
|
1143
|
+
def _build_portal_components(self, components: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
|
|
1144
|
+
resolved_components: list[dict[str, Any]] = []
|
|
1145
|
+
pc_x = 0
|
|
1146
|
+
pc_y = 0
|
|
1147
|
+
pc_row_height = 0
|
|
1148
|
+
mobile_y = 0
|
|
1149
|
+
reserved_keys = {"sectionId", "title", "sourceType", "ordinal", "chartRef", "viewRef", "text", "url"}
|
|
1150
|
+
for component in components:
|
|
1151
|
+
source_type = component.get("sourceType")
|
|
1152
|
+
extra = {key: deepcopy(value) for key, value in component.items() if key not in reserved_keys}
|
|
1153
|
+
position = deepcopy(extra.pop("position", None))
|
|
1154
|
+
if position is None:
|
|
1155
|
+
position, pc_x, pc_y, pc_row_height, mobile_y = _portal_component_position(
|
|
1156
|
+
source_type,
|
|
1157
|
+
pc_x=pc_x,
|
|
1158
|
+
pc_y=pc_y,
|
|
1159
|
+
pc_row_height=pc_row_height,
|
|
1160
|
+
mobile_y=mobile_y,
|
|
1161
|
+
)
|
|
1162
|
+
dash_style = deepcopy(extra.pop("dashStyleConfigBO", None))
|
|
1163
|
+
if source_type == "chart" and component.get("chartRef"):
|
|
1164
|
+
ref = component["chartRef"]
|
|
1165
|
+
chart = store.get_artifact("charts", f"{ref['entity_id']}:{ref['chart_id']}", {})
|
|
1166
|
+
chart_config = {
|
|
1167
|
+
"biChartId": chart.get("bi_chart_id") or chart.get("chart_key"),
|
|
1168
|
+
"chartComponentTitle": component.get("title"),
|
|
1169
|
+
"beingShowTitle": True,
|
|
1170
|
+
}
|
|
1171
|
+
chart_config.update(extra.pop("chartConfig", {}) if isinstance(extra.get("chartConfig"), dict) else {})
|
|
1172
|
+
if not chart_config.get("chartComponentTitle"):
|
|
1173
|
+
chart_config["chartComponentTitle"] = component.get("title")
|
|
1174
|
+
resolved_component: dict[str, Any] = {
|
|
1175
|
+
"type": BI_CHART_COMPONENT_TYPE,
|
|
1176
|
+
"position": position,
|
|
1177
|
+
"chartConfig": _compact_dict(chart_config),
|
|
1178
|
+
}
|
|
1179
|
+
if dash_style is not None:
|
|
1180
|
+
resolved_component["dashStyleConfigBO"] = dash_style
|
|
1181
|
+
resolved_component.update(extra)
|
|
1182
|
+
resolved_components.append(
|
|
1183
|
+
resolved_component
|
|
1184
|
+
)
|
|
1185
|
+
elif source_type == "view" and component.get("viewRef"):
|
|
1186
|
+
ref = component["viewRef"]
|
|
1187
|
+
view = store.get_artifact("views", f"{ref['entity_id']}:{ref['view_id']}", {})
|
|
1188
|
+
app_key = store.get_artifact("apps", ref["entity_id"], {}).get("app_key")
|
|
1189
|
+
view_config = {
|
|
1190
|
+
"appKey": app_key,
|
|
1191
|
+
"viewgraphKey": view.get("viewgraph_key"),
|
|
1192
|
+
"viewgraphName": component.get("title"),
|
|
1193
|
+
"formTitle": self._entity_display_name(store, ref["entity_id"]),
|
|
1194
|
+
"componentTitle": component.get("title"),
|
|
1195
|
+
"viewgraphType": _portal_view_type(store, ref["entity_id"], ref["view_id"]),
|
|
1196
|
+
"beingShowTitle": True,
|
|
1197
|
+
"dataManageStatus": True,
|
|
1198
|
+
}
|
|
1199
|
+
view_config.update(extra.pop("viewgraphConfig", {}) if isinstance(extra.get("viewgraphConfig"), dict) else {})
|
|
1200
|
+
if not view_config.get("componentTitle"):
|
|
1201
|
+
view_config["componentTitle"] = component.get("title")
|
|
1202
|
+
resolved_component = {
|
|
1203
|
+
"type": 10,
|
|
1204
|
+
"position": position,
|
|
1205
|
+
"viewgraphConfig": _compact_dict(view_config),
|
|
1206
|
+
}
|
|
1207
|
+
if dash_style is not None:
|
|
1208
|
+
resolved_component["dashStyleConfigBO"] = dash_style
|
|
1209
|
+
resolved_component.update(extra)
|
|
1210
|
+
resolved_components.append(
|
|
1211
|
+
resolved_component
|
|
1212
|
+
)
|
|
1213
|
+
elif source_type == "grid":
|
|
1214
|
+
raw_grid_config = extra.pop("gridConfig", {}) if isinstance(extra.get("gridConfig"), dict) else {}
|
|
1215
|
+
grid_config = deepcopy(raw_grid_config)
|
|
1216
|
+
raw_items = grid_config.pop("items", None)
|
|
1217
|
+
if raw_items is None:
|
|
1218
|
+
raw_items = extra.pop("items", [])
|
|
1219
|
+
resolved_component = {
|
|
1220
|
+
"type": 2,
|
|
1221
|
+
"position": position,
|
|
1222
|
+
"gridConfig": _compact_dict(
|
|
1223
|
+
{
|
|
1224
|
+
"gridTitle": grid_config.pop("gridTitle", component.get("title")),
|
|
1225
|
+
"beingShowTitle": bool(grid_config.pop("beingShowTitle", True)),
|
|
1226
|
+
"items": self._resolve_grid_items(raw_items if isinstance(raw_items, list) else [], store),
|
|
1227
|
+
**grid_config,
|
|
1228
|
+
}
|
|
1229
|
+
),
|
|
1230
|
+
}
|
|
1231
|
+
if dash_style is not None:
|
|
1232
|
+
resolved_component["dashStyleConfigBO"] = dash_style
|
|
1233
|
+
resolved_component.update(extra)
|
|
1234
|
+
resolved_components.append(resolved_component)
|
|
1235
|
+
elif source_type == "filter":
|
|
1236
|
+
filter_groups = extra.pop("filterConfig", [])
|
|
1237
|
+
graph_list = extra.pop("graphList", [])
|
|
1238
|
+
if isinstance(filter_groups, dict):
|
|
1239
|
+
graph_list = filter_groups.get("graphList", graph_list)
|
|
1240
|
+
filter_groups = filter_groups.get("filterConfig", [])
|
|
1241
|
+
resolved_component = {
|
|
1242
|
+
"type": 6,
|
|
1243
|
+
"position": position,
|
|
1244
|
+
"filterConfig": {
|
|
1245
|
+
"filterConfig": self._resolve_dash_filter_groups(filter_groups, store),
|
|
1246
|
+
"graphList": self._resolve_dash_filter_graphs(graph_list, store),
|
|
1247
|
+
},
|
|
1248
|
+
}
|
|
1249
|
+
if dash_style is not None:
|
|
1250
|
+
resolved_component["dashStyleConfigBO"] = dash_style
|
|
1251
|
+
resolved_component.update(extra)
|
|
1252
|
+
resolved_components.append(resolved_component)
|
|
1253
|
+
elif source_type == "text":
|
|
1254
|
+
text_config = {"text": component.get("text", "")}
|
|
1255
|
+
text_config.update(extra.pop("textConfig", {}) if isinstance(extra.get("textConfig"), dict) else {})
|
|
1256
|
+
resolved_component = {
|
|
1257
|
+
"type": 5,
|
|
1258
|
+
"position": position,
|
|
1259
|
+
"textConfig": text_config,
|
|
1260
|
+
}
|
|
1261
|
+
if dash_style is not None:
|
|
1262
|
+
resolved_component["dashStyleConfigBO"] = dash_style
|
|
1263
|
+
resolved_component.update(extra)
|
|
1264
|
+
resolved_components.append(
|
|
1265
|
+
resolved_component
|
|
1266
|
+
)
|
|
1267
|
+
elif source_type == "link":
|
|
1268
|
+
link_config = {
|
|
1269
|
+
"url": component.get("url", ""),
|
|
1270
|
+
"beingLoginAuth": False,
|
|
1271
|
+
}
|
|
1272
|
+
link_config.update(extra.pop("linkConfig", {}) if isinstance(extra.get("linkConfig"), dict) else {})
|
|
1273
|
+
resolved_component = {
|
|
1274
|
+
"type": 4,
|
|
1275
|
+
"position": position,
|
|
1276
|
+
"linkConfig": link_config,
|
|
1277
|
+
}
|
|
1278
|
+
if dash_style is not None:
|
|
1279
|
+
resolved_component["dashStyleConfigBO"] = dash_style
|
|
1280
|
+
resolved_component.update(extra)
|
|
1281
|
+
resolved_components.append(
|
|
1282
|
+
resolved_component
|
|
1283
|
+
)
|
|
1284
|
+
return resolved_components
|
|
1285
|
+
|
|
1286
|
+
def _resolve_grid_items(self, items: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
|
|
1287
|
+
resolved_items: list[dict[str, Any]] = []
|
|
1288
|
+
for ordinal, item in enumerate(items):
|
|
1289
|
+
resolved = self._resolve_grid_item(item, store, ordinal)
|
|
1290
|
+
if resolved is not None:
|
|
1291
|
+
resolved_items.append(resolved)
|
|
1292
|
+
return resolved_items
|
|
1293
|
+
|
|
1294
|
+
def _resolve_grid_item(self, item: dict[str, Any], store: RunArtifactStore, ordinal: int) -> dict[str, Any] | None:
|
|
1295
|
+
data = deepcopy(item)
|
|
1296
|
+
target_type = str(data.pop("target_type", data.pop("targetType", "app")) or "app").lower()
|
|
1297
|
+
display_title = data.pop("title", None)
|
|
1298
|
+
being_show_title = bool(data.pop("beingShowTitle", True))
|
|
1299
|
+
jump_mode = int(data.pop("jumpMode", 1))
|
|
1300
|
+
icon_size = data.pop("iconSize", "40px")
|
|
1301
|
+
icon_url = data.pop("iconUrl", "")
|
|
1302
|
+
|
|
1303
|
+
if target_type == "app":
|
|
1304
|
+
entity_id = data.pop("entity_id", None) or data.pop("entityId", None)
|
|
1305
|
+
if not entity_id:
|
|
1306
|
+
return None
|
|
1307
|
+
app_key = self._get_app_key(store, str(entity_id))
|
|
1308
|
+
default_title = self._entity_display_name(store, str(entity_id))
|
|
1309
|
+
custom_title = display_title or default_title
|
|
1310
|
+
being_custom_title = bool(data.pop("beingCustomTitle", False) or custom_title != default_title)
|
|
1311
|
+
return _compact_dict(
|
|
1312
|
+
{
|
|
1313
|
+
"ordinal": ordinal,
|
|
1314
|
+
"type": 1,
|
|
1315
|
+
"jumpMode": jump_mode,
|
|
1316
|
+
"iconUrl": icon_url,
|
|
1317
|
+
"title": custom_title,
|
|
1318
|
+
"beingShowTitle": being_show_title,
|
|
1319
|
+
"beingCustomTitle": being_custom_title,
|
|
1320
|
+
"customTitle": custom_title,
|
|
1321
|
+
"linkAppKey": app_key,
|
|
1322
|
+
"linkFormType": int(data.pop("linkFormType", 2)),
|
|
1323
|
+
"iconSize": icon_size,
|
|
1324
|
+
**data,
|
|
1325
|
+
}
|
|
1326
|
+
)
|
|
1327
|
+
if target_type == "portal":
|
|
1328
|
+
dash_key = data.pop("dashKey", None) or store.get_artifact("portal", "dash_key")
|
|
1329
|
+
if not dash_key:
|
|
1330
|
+
return None
|
|
1331
|
+
custom_title = display_title or "门户"
|
|
1332
|
+
return _compact_dict(
|
|
1333
|
+
{
|
|
1334
|
+
"ordinal": ordinal,
|
|
1335
|
+
"type": 2,
|
|
1336
|
+
"jumpMode": jump_mode,
|
|
1337
|
+
"iconUrl": icon_url,
|
|
1338
|
+
"title": custom_title,
|
|
1339
|
+
"beingShowTitle": being_show_title,
|
|
1340
|
+
"beingCustomTitle": bool(data.pop("beingCustomTitle", True)),
|
|
1341
|
+
"customTitle": custom_title,
|
|
1342
|
+
"linkDashKey": dash_key,
|
|
1343
|
+
"iconSize": icon_size,
|
|
1344
|
+
**data,
|
|
1345
|
+
}
|
|
1346
|
+
)
|
|
1347
|
+
if target_type == "package":
|
|
1348
|
+
tag_id = data.pop("tagId", None) or store.get_artifact("package", "tag_id")
|
|
1349
|
+
if tag_id is None:
|
|
1350
|
+
return None
|
|
1351
|
+
custom_title = display_title or "应用包"
|
|
1352
|
+
return _compact_dict(
|
|
1353
|
+
{
|
|
1354
|
+
"ordinal": ordinal,
|
|
1355
|
+
"type": 3,
|
|
1356
|
+
"jumpMode": jump_mode,
|
|
1357
|
+
"iconUrl": icon_url,
|
|
1358
|
+
"title": custom_title,
|
|
1359
|
+
"beingShowTitle": being_show_title,
|
|
1360
|
+
"beingCustomTitle": bool(data.pop("beingCustomTitle", True)),
|
|
1361
|
+
"customTitle": custom_title,
|
|
1362
|
+
"linkTagId": tag_id,
|
|
1363
|
+
"iconSize": icon_size,
|
|
1364
|
+
**data,
|
|
1365
|
+
}
|
|
1366
|
+
)
|
|
1367
|
+
if target_type in {"link", "url", "custom_url"}:
|
|
1368
|
+
url = data.pop("url", None) or data.pop("linkUrl", None)
|
|
1369
|
+
if not url:
|
|
1370
|
+
return None
|
|
1371
|
+
custom_title = display_title or "链接"
|
|
1372
|
+
return _compact_dict(
|
|
1373
|
+
{
|
|
1374
|
+
"ordinal": ordinal,
|
|
1375
|
+
"type": 4,
|
|
1376
|
+
"jumpMode": jump_mode,
|
|
1377
|
+
"iconUrl": icon_url,
|
|
1378
|
+
"title": custom_title,
|
|
1379
|
+
"beingShowTitle": being_show_title,
|
|
1380
|
+
"beingCustomTitle": bool(data.pop("beingCustomTitle", True)),
|
|
1381
|
+
"customTitle": custom_title,
|
|
1382
|
+
"linkUrl": url,
|
|
1383
|
+
"iconSize": icon_size,
|
|
1384
|
+
**data,
|
|
1385
|
+
}
|
|
1386
|
+
)
|
|
1387
|
+
return None
|
|
1388
|
+
|
|
1389
|
+
def _seed_records(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
1390
|
+
if not entity.sample_records:
|
|
1391
|
+
return
|
|
1392
|
+
app_key = self._get_app_key(store, entity.entity_id)
|
|
1393
|
+
field_meta = store.get_artifact("field_maps", entity.entity_id, {}) or {}
|
|
1394
|
+
field_map = field_meta.get("by_field_id", {})
|
|
1395
|
+
record_artifacts = store.get_artifact("records", entity.entity_id, {}) or {}
|
|
1396
|
+
for index, sample_record in enumerate(entity.sample_records, start=1):
|
|
1397
|
+
record_key = sample_record.get("record_id") or f"record_{index}"
|
|
1398
|
+
if record_key in record_artifacts:
|
|
1399
|
+
continue
|
|
1400
|
+
answers = self._build_seed_answers(profile, entity, sample_record.get("values", {}), field_map, store)
|
|
1401
|
+
if not answers:
|
|
1402
|
+
continue
|
|
1403
|
+
result = self.record_tools.record_create(
|
|
1404
|
+
profile=profile,
|
|
1405
|
+
app_key=app_key,
|
|
1406
|
+
answers=answers,
|
|
1407
|
+
submit_type=int(sample_record.get("submit_type", 1)),
|
|
1408
|
+
)
|
|
1409
|
+
apply_result = result.get("result") or {}
|
|
1410
|
+
apply_id = apply_result.get("applyId") or apply_result.get("id")
|
|
1411
|
+
record_artifacts[record_key] = {"apply_id": apply_id, "result": result}
|
|
1412
|
+
store.set_artifact("records", entity.entity_id, record_artifacts)
|
|
1413
|
+
|
|
1414
|
+
def _build_seed_answers(
|
|
1415
|
+
self,
|
|
1416
|
+
profile: str,
|
|
1417
|
+
entity: CompiledEntity,
|
|
1418
|
+
values: dict[str, Any],
|
|
1419
|
+
field_map: dict[str, int],
|
|
1420
|
+
store: RunArtifactStore,
|
|
1421
|
+
) -> list[dict[str, Any]]:
|
|
1422
|
+
answers: list[dict[str, Any]] = []
|
|
1423
|
+
for field_id, raw_value in values.items():
|
|
1424
|
+
field_spec = entity.field_specs.get(field_id)
|
|
1425
|
+
que_id = field_map.get(field_id)
|
|
1426
|
+
if field_spec is None or que_id is None:
|
|
1427
|
+
continue
|
|
1428
|
+
answer = self._build_answer_detail(profile, field_spec, que_id, raw_value, store)
|
|
1429
|
+
if answer is not None:
|
|
1430
|
+
answers.append(answer)
|
|
1431
|
+
return answers
|
|
1432
|
+
|
|
1433
|
+
def _build_answer_detail(
|
|
1434
|
+
self,
|
|
1435
|
+
profile: str,
|
|
1436
|
+
field_spec: dict[str, Any],
|
|
1437
|
+
que_id: int,
|
|
1438
|
+
raw_value: Any,
|
|
1439
|
+
store: RunArtifactStore,
|
|
1440
|
+
) -> dict[str, Any] | None:
|
|
1441
|
+
field_type = FieldType(field_spec["type"])
|
|
1442
|
+
if raw_value is None:
|
|
1443
|
+
return None
|
|
1444
|
+
if field_type == FieldType.relation:
|
|
1445
|
+
references = raw_value if isinstance(raw_value, list) else [raw_value]
|
|
1446
|
+
values = []
|
|
1447
|
+
for record_ref in references:
|
|
1448
|
+
apply_id = self._resolve_sample_record_apply_id(field_spec["target_entity_id"], record_ref, store)
|
|
1449
|
+
if apply_id is not None:
|
|
1450
|
+
values.append({"value": str(apply_id)})
|
|
1451
|
+
if not values:
|
|
1452
|
+
return None
|
|
1453
|
+
return {"queId": que_id, "queType": 25, "values": values, "tableValues": []}
|
|
1454
|
+
if field_type == FieldType.multi_select:
|
|
1455
|
+
items = raw_value if isinstance(raw_value, list) else [raw_value]
|
|
1456
|
+
return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [{"value": str(item)} for item in items], "tableValues": []}
|
|
1457
|
+
if field_type == FieldType.boolean:
|
|
1458
|
+
return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [{"value": "是" if bool(raw_value) else "否"}], "tableValues": []}
|
|
1459
|
+
if field_type == FieldType.member:
|
|
1460
|
+
member_value = self._resolve_seed_member_value(profile, raw_value)
|
|
1461
|
+
if member_value is None:
|
|
1462
|
+
return None
|
|
1463
|
+
return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [member_value], "tableValues": []}
|
|
1464
|
+
if field_type == FieldType.attachment:
|
|
1465
|
+
items = raw_value if isinstance(raw_value, list) else [raw_value]
|
|
1466
|
+
return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": items, "tableValues": []}
|
|
1467
|
+
if field_type in {FieldType.subtable, FieldType.department}:
|
|
1468
|
+
return None
|
|
1469
|
+
return {"queId": que_id, "queType": QUESTION_TYPE_MAP[field_type], "values": [{"value": str(raw_value)}], "tableValues": []}
|
|
1470
|
+
|
|
1471
|
+
def _resolve_seed_member_value(self, profile: str, raw_value: Any) -> dict[str, Any] | None:
|
|
1472
|
+
if isinstance(raw_value, dict):
|
|
1473
|
+
member_id = raw_value.get("id") or raw_value.get("uid")
|
|
1474
|
+
if not isinstance(member_id, int) or member_id <= 0:
|
|
1475
|
+
return None
|
|
1476
|
+
value = raw_value.get("value") or raw_value.get("name") or str(member_id)
|
|
1477
|
+
member_payload = {
|
|
1478
|
+
"id": member_id,
|
|
1479
|
+
"value": str(value),
|
|
1480
|
+
}
|
|
1481
|
+
if raw_value.get("email"):
|
|
1482
|
+
member_payload["email"] = raw_value.get("email")
|
|
1483
|
+
if raw_value.get("otherInfo") or raw_value.get("other_info"):
|
|
1484
|
+
member_payload["otherInfo"] = raw_value.get("otherInfo") or raw_value.get("other_info")
|
|
1485
|
+
return member_payload
|
|
1486
|
+
if not isinstance(raw_value, int) or raw_value <= 0:
|
|
1487
|
+
return None
|
|
1488
|
+
session_profile = self.record_tools.sessions.get_profile(profile)
|
|
1489
|
+
value = str(raw_value)
|
|
1490
|
+
member_payload: dict[str, Any] = {"id": raw_value, "value": value}
|
|
1491
|
+
if session_profile is not None and raw_value == session_profile.uid:
|
|
1492
|
+
member_payload["value"] = session_profile.nick_name or session_profile.email or value
|
|
1493
|
+
if session_profile.email:
|
|
1494
|
+
member_payload["email"] = session_profile.email
|
|
1495
|
+
return member_payload
|
|
1496
|
+
|
|
1497
|
+
def _resolve_sample_record_apply_id(self, entity_id: str, record_ref: Any, store: RunArtifactStore) -> int | None:
|
|
1498
|
+
record_key = str(record_ref)
|
|
1499
|
+
entity_records = store.get_artifact("records", entity_id, {}) or {}
|
|
1500
|
+
apply_id = (entity_records.get(record_key) or {}).get("apply_id")
|
|
1501
|
+
return int(apply_id) if apply_id else None
|
|
1502
|
+
|
|
1503
|
+
def _entity_from_step(self, compiled: CompiledSolution, step_name: str) -> CompiledEntity:
|
|
1504
|
+
entity_id = step_name.split(".")[-1]
|
|
1505
|
+
for entity in compiled.entities:
|
|
1506
|
+
if entity.entity_id == entity_id:
|
|
1507
|
+
return entity
|
|
1508
|
+
raise QingflowApiError.config_error(f"unknown entity '{entity_id}' in execution plan")
|
|
1509
|
+
|
|
1510
|
+
def _role_from_step(self, compiled: CompiledSolution, step_name: str) -> CompiledRole:
|
|
1511
|
+
role_id = step_name.split(".")[-1]
|
|
1512
|
+
for role in compiled.roles:
|
|
1513
|
+
if role.role_id == role_id:
|
|
1514
|
+
return role
|
|
1515
|
+
raise QingflowApiError.config_error(f"unknown role '{role_id}' in execution plan")
|
|
1516
|
+
|
|
1517
|
+
def _get_app_key(self, store: RunArtifactStore, entity_id: str) -> str:
|
|
1518
|
+
app_key = store.get_artifact("apps", entity_id, {}).get("app_key")
|
|
1519
|
+
if not app_key:
|
|
1520
|
+
raise QingflowApiError.config_error(f"app_key for entity '{entity_id}' is missing")
|
|
1521
|
+
return app_key
|
|
1522
|
+
|
|
1523
|
+
def _get_edit_version_no(self, store: RunArtifactStore, entity_id: str) -> int | None:
|
|
1524
|
+
app_artifact = store.get_artifact("apps", entity_id, {}) or {}
|
|
1525
|
+
edit_version_no = app_artifact.get("edit_version_no")
|
|
1526
|
+
if edit_version_no is not None:
|
|
1527
|
+
return int(edit_version_no)
|
|
1528
|
+
field_meta = store.get_artifact("field_maps", entity_id, {}) or {}
|
|
1529
|
+
edit_version_no = field_meta.get("edit_version_no")
|
|
1530
|
+
if edit_version_no is not None:
|
|
1531
|
+
return int(edit_version_no)
|
|
1532
|
+
return None
|
|
1533
|
+
|
|
1534
|
+
def _set_edit_version_no(self, store: RunArtifactStore, entity_id: str, edit_version_no: int) -> None:
|
|
1535
|
+
app_artifact = store.get_artifact("apps", entity_id, {}) or {}
|
|
1536
|
+
app_artifact["edit_version_no"] = int(edit_version_no)
|
|
1537
|
+
store.set_artifact("apps", entity_id, app_artifact)
|
|
1538
|
+
|
|
1539
|
+
def _ensure_edit_version(
|
|
1540
|
+
self,
|
|
1541
|
+
profile: str,
|
|
1542
|
+
entity_id: str,
|
|
1543
|
+
store: RunArtifactStore,
|
|
1544
|
+
*,
|
|
1545
|
+
app_key: str | None = None,
|
|
1546
|
+
force_new: bool = False,
|
|
1547
|
+
) -> int | None:
|
|
1548
|
+
current = None if force_new else self._get_edit_version_no(store, entity_id)
|
|
1549
|
+
if current is not None:
|
|
1550
|
+
return current
|
|
1551
|
+
effective_app_key = app_key or self._get_app_key(store, entity_id)
|
|
1552
|
+
version_result = self.app_tools.app_get_edit_version_no(profile=profile, app_key=effective_app_key).get("result") or {}
|
|
1553
|
+
edit_version_no = version_result.get("editVersionNo") or version_result.get("versionNo")
|
|
1554
|
+
if edit_version_no is None:
|
|
1555
|
+
return None
|
|
1556
|
+
self._set_edit_version_no(store, entity_id, int(edit_version_no))
|
|
1557
|
+
return int(edit_version_no)
|
|
1558
|
+
|
|
1559
|
+
def _get_field_map(self, store: RunArtifactStore, entity_id: str) -> dict[str, int]:
|
|
1560
|
+
return store.get_artifact("field_maps", entity_id, {}).get("by_field_id", {})
|
|
1561
|
+
|
|
1562
|
+
def _publish_payload(self, store: RunArtifactStore, entity_id: str) -> dict[str, Any]:
|
|
1563
|
+
edit_version_no = self._get_edit_version_no(store, entity_id) or 1
|
|
1564
|
+
return {"editVersionNo": int(edit_version_no)}
|
|
1565
|
+
|
|
1566
|
+
def _target_entity_id_from_label(self, entity: CompiledEntity, label: str) -> str | None:
|
|
1567
|
+
for field in entity.field_specs.values():
|
|
1568
|
+
if field["label"] == label and field.get("target_entity_id"):
|
|
1569
|
+
return field["target_entity_id"]
|
|
1570
|
+
return None
|
|
1571
|
+
|
|
1572
|
+
def _target_field_label(self, entity: CompiledEntity, label: str, store: RunArtifactStore) -> str:
|
|
1573
|
+
for field in entity.field_specs.values():
|
|
1574
|
+
if field["label"] == label and field.get("target_field_id"):
|
|
1575
|
+
target_field_id = field["target_field_id"]
|
|
1576
|
+
target_entity_id = field["target_entity_id"]
|
|
1577
|
+
return self._field_label_from_id(target_entity_id, target_field_id, store)
|
|
1578
|
+
return "名称"
|
|
1579
|
+
|
|
1580
|
+
def _target_field_id_from_label(self, entity: CompiledEntity, label: str) -> str:
|
|
1581
|
+
for field in entity.field_specs.values():
|
|
1582
|
+
if field["label"] == label and field.get("target_field_id"):
|
|
1583
|
+
return field["target_field_id"]
|
|
1584
|
+
return "title"
|
|
1585
|
+
|
|
1586
|
+
def _field_label_from_id(self, entity_id: str, field_id: str, store: RunArtifactStore | None) -> str:
|
|
1587
|
+
if store is not None:
|
|
1588
|
+
entity_field_map = store.data.get("normalized_solution_spec", {}).get("entities", [])
|
|
1589
|
+
for entity in entity_field_map:
|
|
1590
|
+
if entity["entity_id"] == entity_id:
|
|
1591
|
+
for field in entity["fields"]:
|
|
1592
|
+
if field["field_id"] == field_id:
|
|
1593
|
+
return field["label"]
|
|
1594
|
+
return "名称" if field_id == "title" else field_id
|
|
1595
|
+
|
|
1596
|
+
def _entity_display_name(self, store: RunArtifactStore, target_entity_id: str) -> str:
|
|
1597
|
+
for entity in store.data.get("normalized_solution_spec", {}).get("entities", []):
|
|
1598
|
+
if entity["entity_id"] == target_entity_id:
|
|
1599
|
+
return entity["display_name"]
|
|
1600
|
+
return target_entity_id
|
|
1601
|
+
|
|
1602
|
+
def _entity_data(self, store: RunArtifactStore, entity_id: str) -> dict[str, Any]:
|
|
1603
|
+
for entity in store.data.get("normalized_solution_spec", {}).get("entities", []):
|
|
1604
|
+
if entity["entity_id"] == entity_id:
|
|
1605
|
+
return entity
|
|
1606
|
+
return {}
|
|
1607
|
+
|
|
1608
|
+
def _field_spec_from_store(self, store: RunArtifactStore, entity_id: str, field_id: str) -> dict[str, Any]:
|
|
1609
|
+
entity = self._entity_data(store, entity_id)
|
|
1610
|
+
for field in entity.get("fields", []):
|
|
1611
|
+
if field.get("field_id") == field_id:
|
|
1612
|
+
return field
|
|
1613
|
+
return {}
|
|
1614
|
+
|
|
1615
|
+
def _form_stage_error(
|
|
1616
|
+
self,
|
|
1617
|
+
exc: Exception,
|
|
1618
|
+
*,
|
|
1619
|
+
stage_name: str,
|
|
1620
|
+
entity: CompiledEntity,
|
|
1621
|
+
app_key: str,
|
|
1622
|
+
payload: dict[str, Any],
|
|
1623
|
+
) -> QingflowApiError:
|
|
1624
|
+
nested = _coerce_nested_error_payload(exc)
|
|
1625
|
+
category = str(nested.get("category") or "runtime")
|
|
1626
|
+
message = str(nested.get("message") or f"{stage_name} update failed")
|
|
1627
|
+
return QingflowApiError(
|
|
1628
|
+
category=category,
|
|
1629
|
+
message=message,
|
|
1630
|
+
backend_code=nested.get("backend_code"),
|
|
1631
|
+
request_id=nested.get("request_id"),
|
|
1632
|
+
http_status=nested.get("http_status"),
|
|
1633
|
+
details={
|
|
1634
|
+
"stage": stage_name,
|
|
1635
|
+
"entity_id": entity.entity_id,
|
|
1636
|
+
"display_name": entity.display_name,
|
|
1637
|
+
"app_key": app_key,
|
|
1638
|
+
"field_count": len(entity.field_specs),
|
|
1639
|
+
"field_catalog": _compiled_field_catalog(entity),
|
|
1640
|
+
"form_payload_summary": _summarize_form_payload(payload),
|
|
1641
|
+
"backend_error": nested or None,
|
|
1642
|
+
},
|
|
1643
|
+
)
|
|
1644
|
+
|
|
1645
|
+
def _resolve_dash_filter_graphs(self, graph_list: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
|
|
1646
|
+
resolved_graphs: list[dict[str, Any]] = []
|
|
1647
|
+
for graph in graph_list:
|
|
1648
|
+
data = deepcopy(graph)
|
|
1649
|
+
graph_ref = data.pop("graphRef", None) if isinstance(data.get("graphRef"), dict) else {}
|
|
1650
|
+
entity_id = data.pop("entity_id", None) or graph_ref.get("entity_id")
|
|
1651
|
+
chart_id = data.pop("chart_id", None) or graph_ref.get("chart_id")
|
|
1652
|
+
view_id = data.pop("view_id", None) or graph_ref.get("view_id")
|
|
1653
|
+
graph_type = str(data.get("graphType") or graph_ref.get("graphType") or ("VIEW" if view_id else "CHART")).upper()
|
|
1654
|
+
if entity_id and chart_id and not data.get("graphKey"):
|
|
1655
|
+
chart = store.get_artifact("charts", f"{entity_id}:{chart_id}", {})
|
|
1656
|
+
chart_info = chart.get("chart_info", {})
|
|
1657
|
+
data["graphKey"] = chart.get("bi_chart_id") or chart.get("chart_key")
|
|
1658
|
+
data.setdefault("graphName", chart_info.get("chartName") or chart_id)
|
|
1659
|
+
if entity_id and view_id and not data.get("graphKey"):
|
|
1660
|
+
view = store.get_artifact("views", f"{entity_id}:{view_id}", {})
|
|
1661
|
+
data["graphKey"] = view.get("viewgraph_key")
|
|
1662
|
+
data.setdefault("graphName", self._entity_display_name(store, entity_id))
|
|
1663
|
+
data["graphType"] = graph_type
|
|
1664
|
+
if data.get("graphKey"):
|
|
1665
|
+
resolved_graphs.append(_compact_dict(data))
|
|
1666
|
+
return resolved_graphs
|
|
1667
|
+
|
|
1668
|
+
def _resolve_dash_filter_groups(self, groups: list[dict[str, Any]], store: RunArtifactStore) -> list[dict[str, Any]]:
|
|
1669
|
+
resolved_groups: list[dict[str, Any]] = []
|
|
1670
|
+
for group in groups:
|
|
1671
|
+
group_data = deepcopy(group)
|
|
1672
|
+
resolved_items: list[dict[str, Any]] = []
|
|
1673
|
+
for item in group_data.get("filterGroupConfig", []):
|
|
1674
|
+
item_data = deepcopy(item)
|
|
1675
|
+
conditions = item_data.get("filterCondition", []) or []
|
|
1676
|
+
item_data["filterCondition"] = [
|
|
1677
|
+
resolved
|
|
1678
|
+
for condition in conditions
|
|
1679
|
+
if (resolved := self._resolve_dash_filter_condition(condition, store)) is not None
|
|
1680
|
+
]
|
|
1681
|
+
resolved_items.append(item_data)
|
|
1682
|
+
group_data["filterGroupConfig"] = resolved_items
|
|
1683
|
+
resolved_groups.append(group_data)
|
|
1684
|
+
return resolved_groups
|
|
1685
|
+
|
|
1686
|
+
def _resolve_dash_filter_condition(self, condition: dict[str, Any], store: RunArtifactStore) -> dict[str, Any] | None:
|
|
1687
|
+
data = deepcopy(condition)
|
|
1688
|
+
graph_ref = data.pop("graphRef", None) if isinstance(data.get("graphRef"), dict) else {}
|
|
1689
|
+
entity_id = data.pop("entity_id", None) or graph_ref.get("entity_id")
|
|
1690
|
+
field_id = data.pop("field_id", None) or graph_ref.get("field_id")
|
|
1691
|
+
chart_id = data.pop("chart_id", None) or graph_ref.get("chart_id")
|
|
1692
|
+
view_id = data.pop("view_id", None) or graph_ref.get("view_id")
|
|
1693
|
+
graph_type = str(data.get("graphType") or graph_ref.get("graphType") or ("VIEW" if view_id else "CHART")).upper()
|
|
1694
|
+
if entity_id and field_id:
|
|
1695
|
+
field_meta = store.get_artifact("field_maps", entity_id, {})
|
|
1696
|
+
field_spec = self._field_spec_from_store(store, entity_id, field_id)
|
|
1697
|
+
field_label = field_spec.get("label") or self._field_label_from_id(entity_id, field_id, store)
|
|
1698
|
+
qingflow_field_type = field_spec.get("type")
|
|
1699
|
+
if qingflow_field_type:
|
|
1700
|
+
try:
|
|
1701
|
+
data.setdefault("queType", QUESTION_TYPE_MAP[FieldType(qingflow_field_type)])
|
|
1702
|
+
except (KeyError, ValueError):
|
|
1703
|
+
pass
|
|
1704
|
+
data.setdefault("queTitle", field_label)
|
|
1705
|
+
data.setdefault("queId", field_meta.get("by_field_id", {}).get(field_id))
|
|
1706
|
+
data.setdefault("queOriginType", data.get("queType"))
|
|
1707
|
+
if qingflow_field_type == "date":
|
|
1708
|
+
data.setdefault("dateType", 0)
|
|
1709
|
+
elif qingflow_field_type == "datetime":
|
|
1710
|
+
data.setdefault("dateType", 1)
|
|
1711
|
+
if entity_id and chart_id and not data.get("chartKey"):
|
|
1712
|
+
chart = store.get_artifact("charts", f"{entity_id}:{chart_id}", {})
|
|
1713
|
+
chart_info = chart.get("chart_info", {})
|
|
1714
|
+
data["chartKey"] = chart.get("bi_chart_id") or chart.get("chart_key")
|
|
1715
|
+
bi_field_id = store.get_artifact("field_maps", entity_id, {}).get("bi_by_field_id", {}).get(field_id)
|
|
1716
|
+
if bi_field_id:
|
|
1717
|
+
data["biFieldId"] = bi_field_id
|
|
1718
|
+
data.setdefault("chartName", chart_info.get("chartName") or chart_id)
|
|
1719
|
+
if entity_id and view_id and not data.get("chartKey"):
|
|
1720
|
+
view = store.get_artifact("views", f"{entity_id}:{view_id}", {})
|
|
1721
|
+
data["chartKey"] = view.get("viewgraph_key")
|
|
1722
|
+
data.setdefault("chartName", self._entity_display_name(store, entity_id))
|
|
1723
|
+
data["graphType"] = graph_type
|
|
1724
|
+
if not data.get("chartKey") or data.get("queId") is None:
|
|
1725
|
+
return None
|
|
1726
|
+
return _compact_dict(data)
|
|
1727
|
+
|
|
1728
|
+
def _resolve_chart_headers(self, headers: list[dict[str, Any]], field_map: dict[str, int]) -> list[dict[str, Any]]:
|
|
1729
|
+
resolved_headers: list[dict[str, Any]] = []
|
|
1730
|
+
for header in headers:
|
|
1731
|
+
field_id = header.get("field_id")
|
|
1732
|
+
que_id = field_map.get(field_id) if field_id else header.get("queId")
|
|
1733
|
+
if que_id is None:
|
|
1734
|
+
continue
|
|
1735
|
+
resolved = deepcopy(header)
|
|
1736
|
+
resolved["queId"] = que_id
|
|
1737
|
+
resolved.pop("field_id", None)
|
|
1738
|
+
resolved_headers.append(resolved)
|
|
1739
|
+
return resolved_headers
|
|
1740
|
+
|
|
1741
|
+
def _resolve_chart_targets(self, targets: list[dict[str, Any]], field_map: dict[str, int]) -> list[dict[str, Any]]:
|
|
1742
|
+
resolved_targets: list[dict[str, Any]] = []
|
|
1743
|
+
for target in targets:
|
|
1744
|
+
field_id = target.get("field_id")
|
|
1745
|
+
que_id = field_map.get(field_id) if field_id else target.get("queId")
|
|
1746
|
+
if que_id is None and not target.get("createByFormula"):
|
|
1747
|
+
continue
|
|
1748
|
+
resolved = deepcopy(target)
|
|
1749
|
+
resolved["queId"] = que_id
|
|
1750
|
+
resolved["aggreType"] = self._chart_aggre_type(str(target.get("aggregate") or "count").lower())
|
|
1751
|
+
resolved.setdefault("createByFormula", False)
|
|
1752
|
+
resolved.setdefault("numberFormat", 1)
|
|
1753
|
+
resolved.setdefault("currencyType", 1)
|
|
1754
|
+
resolved.setdefault("decimalDigit", 0)
|
|
1755
|
+
resolved.setdefault("sort", "none")
|
|
1756
|
+
resolved.pop("field_id", None)
|
|
1757
|
+
resolved.pop("aggregate", None)
|
|
1758
|
+
resolved_targets.append(resolved)
|
|
1759
|
+
return resolved_targets
|
|
1760
|
+
|
|
1761
|
+
def _chart_aggre_type(self, aggregate: str) -> int:
|
|
1762
|
+
return {
|
|
1763
|
+
"sum": 0,
|
|
1764
|
+
"average": 1,
|
|
1765
|
+
"avg": 1,
|
|
1766
|
+
"max": 2,
|
|
1767
|
+
"min": 3,
|
|
1768
|
+
"first": 4,
|
|
1769
|
+
"last": 5,
|
|
1770
|
+
"count": 6,
|
|
1771
|
+
"count_all": 8,
|
|
1772
|
+
}.get(aggregate, 6)
|
|
1773
|
+
|
|
1774
|
+
def _resolve_qingbi_dimension_fields(
|
|
1775
|
+
self,
|
|
1776
|
+
field_ids: list[str],
|
|
1777
|
+
*,
|
|
1778
|
+
entity: CompiledEntity,
|
|
1779
|
+
field_map: dict[str, int],
|
|
1780
|
+
bi_field_map: dict[str, str],
|
|
1781
|
+
qingbi_fields_by_id: dict[str, dict[str, Any]],
|
|
1782
|
+
) -> list[dict[str, Any]]:
|
|
1783
|
+
dimensions: list[dict[str, Any]] = []
|
|
1784
|
+
for field_id in field_ids:
|
|
1785
|
+
resolved = self._resolve_qingbi_field(
|
|
1786
|
+
entity=entity,
|
|
1787
|
+
field_id=field_id,
|
|
1788
|
+
field_map=field_map,
|
|
1789
|
+
bi_field_map=bi_field_map,
|
|
1790
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
1791
|
+
field_usage="dimension",
|
|
1792
|
+
aggregate="sum",
|
|
1793
|
+
)
|
|
1794
|
+
if resolved is not None:
|
|
1795
|
+
dimensions.append(resolved)
|
|
1796
|
+
return dimensions
|
|
1797
|
+
|
|
1798
|
+
def _resolve_qingbi_metric_fields(
|
|
1799
|
+
self,
|
|
1800
|
+
field_ids: list[str],
|
|
1801
|
+
*,
|
|
1802
|
+
aggregate: str,
|
|
1803
|
+
entity: CompiledEntity,
|
|
1804
|
+
field_map: dict[str, int],
|
|
1805
|
+
bi_field_map: dict[str, str],
|
|
1806
|
+
qingbi_fields_by_id: dict[str, dict[str, Any]],
|
|
1807
|
+
) -> list[dict[str, Any]]:
|
|
1808
|
+
if aggregate == "count" or not field_ids:
|
|
1809
|
+
return [self._default_qingbi_total_metric()]
|
|
1810
|
+
metrics: list[dict[str, Any]] = []
|
|
1811
|
+
for field_id in field_ids:
|
|
1812
|
+
resolved = self._resolve_qingbi_field(
|
|
1813
|
+
entity=entity,
|
|
1814
|
+
field_id=field_id,
|
|
1815
|
+
field_map=field_map,
|
|
1816
|
+
bi_field_map=bi_field_map,
|
|
1817
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
1818
|
+
field_usage="metric",
|
|
1819
|
+
aggregate=aggregate,
|
|
1820
|
+
)
|
|
1821
|
+
if resolved is not None:
|
|
1822
|
+
metrics.append(resolved)
|
|
1823
|
+
return metrics or [self._default_qingbi_total_metric()]
|
|
1824
|
+
|
|
1825
|
+
def _resolve_qingbi_field(
|
|
1826
|
+
self,
|
|
1827
|
+
*,
|
|
1828
|
+
entity: CompiledEntity,
|
|
1829
|
+
field_id: str,
|
|
1830
|
+
field_map: dict[str, int],
|
|
1831
|
+
bi_field_map: dict[str, str],
|
|
1832
|
+
qingbi_fields_by_id: dict[str, dict[str, Any]],
|
|
1833
|
+
field_usage: str,
|
|
1834
|
+
aggregate: str,
|
|
1835
|
+
) -> dict[str, Any] | None:
|
|
1836
|
+
que_id = field_map.get(field_id)
|
|
1837
|
+
bi_field_id = bi_field_map.get(field_id)
|
|
1838
|
+
if que_id is None or not bi_field_id:
|
|
1839
|
+
return None
|
|
1840
|
+
field_spec = entity.field_specs.get(field_id, {})
|
|
1841
|
+
field_label = entity.field_labels.get(field_id, field_id)
|
|
1842
|
+
qingbi_field = deepcopy(qingbi_fields_by_id.get(bi_field_id, {}))
|
|
1843
|
+
field_type = qingbi_field.get("fieldType") or self._qingbi_field_type_from_spec(field_spec.get("type"))
|
|
1844
|
+
data = {
|
|
1845
|
+
"fieldId": bi_field_id,
|
|
1846
|
+
"fieldName": qingbi_field.get("fieldName") or field_label,
|
|
1847
|
+
"fieldType": field_type,
|
|
1848
|
+
"orderType": "default",
|
|
1849
|
+
"alignType": "left",
|
|
1850
|
+
"dateFormat": "yyyy-MM-dd",
|
|
1851
|
+
"numberFormat": "default",
|
|
1852
|
+
"numberConfig": {
|
|
1853
|
+
"format": "splitter",
|
|
1854
|
+
"unit": "DEFAULT",
|
|
1855
|
+
"prefix": "",
|
|
1856
|
+
"suffix": "",
|
|
1857
|
+
"digit": None,
|
|
1858
|
+
},
|
|
1859
|
+
"digit": None,
|
|
1860
|
+
"aggreType": self._qingbi_aggregate_type(aggregate if field_usage == "metric" else "sum"),
|
|
1861
|
+
"orderPriority": None,
|
|
1862
|
+
"width": None,
|
|
1863
|
+
"verticalAlign": "middle",
|
|
1864
|
+
"formula": qingbi_field.get("formula"),
|
|
1865
|
+
"fieldSource": qingbi_field.get("fieldSource") or "default",
|
|
1866
|
+
"status": qingbi_field.get("status"),
|
|
1867
|
+
"supId": qingbi_field.get("supId"),
|
|
1868
|
+
"beingTable": bool(qingbi_field.get("beingTable", False)),
|
|
1869
|
+
"returnType": qingbi_field.get("returnType"),
|
|
1870
|
+
}
|
|
1871
|
+
return data
|
|
1872
|
+
|
|
1873
|
+
def _default_qingbi_total_metric(self) -> dict[str, Any]:
|
|
1874
|
+
return {
|
|
1875
|
+
"fieldId": ":-100",
|
|
1876
|
+
"fieldName": "数据总量",
|
|
1877
|
+
"fieldType": "decimal",
|
|
1878
|
+
"orderType": "default",
|
|
1879
|
+
"alignType": "left",
|
|
1880
|
+
"dateFormat": "yyyy-MM-dd",
|
|
1881
|
+
"numberFormat": "default",
|
|
1882
|
+
"numberConfig": {
|
|
1883
|
+
"format": "splitter",
|
|
1884
|
+
"unit": "DEFAULT",
|
|
1885
|
+
"prefix": "",
|
|
1886
|
+
"suffix": "",
|
|
1887
|
+
"digit": None,
|
|
1888
|
+
},
|
|
1889
|
+
"digit": None,
|
|
1890
|
+
"aggreType": "sum",
|
|
1891
|
+
"orderPriority": None,
|
|
1892
|
+
"width": None,
|
|
1893
|
+
"verticalAlign": "middle",
|
|
1894
|
+
"beingTable": False,
|
|
1895
|
+
"supId": None,
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
def _qingbi_aggregate_type(self, aggregate: str) -> str:
|
|
1899
|
+
return {
|
|
1900
|
+
"sum": "sum",
|
|
1901
|
+
"avg": "avg",
|
|
1902
|
+
"average": "avg",
|
|
1903
|
+
"max": "max",
|
|
1904
|
+
"min": "min",
|
|
1905
|
+
"count": "sum",
|
|
1906
|
+
"distinct_count": "sum",
|
|
1907
|
+
}.get(aggregate, "sum")
|
|
1908
|
+
|
|
1909
|
+
def _qingbi_field_type_from_spec(self, field_type: str | None) -> str:
|
|
1910
|
+
return {
|
|
1911
|
+
"single_select": "singleSelect",
|
|
1912
|
+
"multi_select": "multiSelect",
|
|
1913
|
+
"member": "member",
|
|
1914
|
+
"department": "dept",
|
|
1915
|
+
"date": "datetime",
|
|
1916
|
+
"datetime": "datetime",
|
|
1917
|
+
"number": "decimal",
|
|
1918
|
+
"amount": "decimal",
|
|
1919
|
+
"boolean": "singleSelect",
|
|
1920
|
+
}.get(str(field_type or ""), "string")
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
def extract_field_map(schema: dict[str, Any]) -> dict[str, int]:
|
|
1924
|
+
result: dict[str, int] = {}
|
|
1925
|
+
for line in schema.get("formQues", []):
|
|
1926
|
+
_extract_question_line(line, result, None)
|
|
1927
|
+
return result
|
|
1928
|
+
|
|
1929
|
+
|
|
1930
|
+
def _extract_question_line(questions: list[dict[str, Any]], result: dict[str, int], parent_title: str | None) -> None:
|
|
1931
|
+
for question in questions:
|
|
1932
|
+
title = question.get("queTitle")
|
|
1933
|
+
que_id = question.get("queId")
|
|
1934
|
+
que_type = question.get("queType")
|
|
1935
|
+
if title and que_id and que_type != 24:
|
|
1936
|
+
composite_title = title if parent_title is None else f"{parent_title}.{title}"
|
|
1937
|
+
result[composite_title] = que_id
|
|
1938
|
+
result.setdefault(title, que_id)
|
|
1939
|
+
for sub_question in question.get("subQuestions", []) or []:
|
|
1940
|
+
sub_title = sub_question.get("queTitle")
|
|
1941
|
+
sub_que_id = sub_question.get("queId")
|
|
1942
|
+
if title and sub_title and sub_que_id:
|
|
1943
|
+
result[f"{title}.{sub_title}"] = sub_que_id
|
|
1944
|
+
result.setdefault(sub_title, sub_que_id)
|
|
1945
|
+
for inner_row in question.get("innerQuestions", []) or []:
|
|
1946
|
+
_extract_question_line(inner_row, result, title if que_type == 24 else parent_title)
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
def _compact_dict(data: dict[str, Any]) -> dict[str, Any]:
|
|
1950
|
+
return {key: value for key, value in data.items() if value is not None}
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
def _extract_workflow_node_id(result: Any, *, expected_type: int | None = None) -> int | None:
|
|
1954
|
+
if expected_type is not None:
|
|
1955
|
+
exact_match = _extract_workflow_node_id_exact(result, expected_type=expected_type)
|
|
1956
|
+
if exact_match is not None:
|
|
1957
|
+
return exact_match
|
|
1958
|
+
return _extract_workflow_node_id_exact(result, expected_type=None)
|
|
1959
|
+
|
|
1960
|
+
|
|
1961
|
+
def _extract_workflow_node_id_exact(result: Any, *, expected_type: int | None) -> int | None:
|
|
1962
|
+
if isinstance(result, dict):
|
|
1963
|
+
node_id = result.get("auditNodeId")
|
|
1964
|
+
node_type = result.get("type")
|
|
1965
|
+
if isinstance(node_id, int) and (expected_type is None or node_type == expected_type):
|
|
1966
|
+
return node_id
|
|
1967
|
+
for node in result.values():
|
|
1968
|
+
extracted = _extract_workflow_node_id_exact(node, expected_type=expected_type)
|
|
1969
|
+
if extracted is not None:
|
|
1970
|
+
return extracted
|
|
1971
|
+
if isinstance(result, list):
|
|
1972
|
+
for node in result:
|
|
1973
|
+
extracted = _extract_workflow_node_id_exact(node, expected_type=expected_type)
|
|
1974
|
+
if extracted is not None:
|
|
1975
|
+
return extracted
|
|
1976
|
+
return None
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
def _extract_workflow_branch_lane_ids(result: Any) -> list[int]:
|
|
1980
|
+
if isinstance(result, dict):
|
|
1981
|
+
branches = result.get("branches")
|
|
1982
|
+
if isinstance(branches, list):
|
|
1983
|
+
lane_ids = [
|
|
1984
|
+
branch.get("auditNodeId")
|
|
1985
|
+
for branch in branches
|
|
1986
|
+
if isinstance(branch, dict) and isinstance(branch.get("auditNodeId"), int)
|
|
1987
|
+
]
|
|
1988
|
+
if lane_ids:
|
|
1989
|
+
return lane_ids
|
|
1990
|
+
for node in result.values():
|
|
1991
|
+
lane_ids = _extract_workflow_branch_lane_ids(node)
|
|
1992
|
+
if lane_ids:
|
|
1993
|
+
return lane_ids
|
|
1994
|
+
if isinstance(result, list):
|
|
1995
|
+
for node in result:
|
|
1996
|
+
lane_ids = _extract_workflow_branch_lane_ids(node)
|
|
1997
|
+
if lane_ids:
|
|
1998
|
+
return lane_ids
|
|
1999
|
+
return []
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
def _branch_lane_ref(branch_node_id: str, branch_index: int) -> str:
|
|
2003
|
+
return f"__branch_lane__{branch_node_id}__{branch_index}"
|
|
2004
|
+
|
|
2005
|
+
|
|
2006
|
+
def _coerce_workflow_nodes(result: Any) -> dict[int, dict[str, Any]]:
|
|
2007
|
+
if not isinstance(result, dict):
|
|
2008
|
+
return {}
|
|
2009
|
+
nodes: dict[int, dict[str, Any]] = {}
|
|
2010
|
+
for node_id, node in result.items():
|
|
2011
|
+
try:
|
|
2012
|
+
parsed_node_id = int(node_id)
|
|
2013
|
+
except (TypeError, ValueError):
|
|
2014
|
+
continue
|
|
2015
|
+
if isinstance(node, dict):
|
|
2016
|
+
nodes[parsed_node_id] = node
|
|
2017
|
+
return nodes
|
|
2018
|
+
|
|
2019
|
+
|
|
2020
|
+
def _workflow_node_is_branch(nodes: dict[int, dict[str, Any]], node_id: int | None) -> bool:
|
|
2021
|
+
if node_id is None:
|
|
2022
|
+
return False
|
|
2023
|
+
node = nodes.get(int(node_id))
|
|
2024
|
+
return isinstance(node, dict) and node.get("type") == 1
|
|
2025
|
+
|
|
2026
|
+
|
|
2027
|
+
def _find_branch_lane_ids(nodes: dict[int, dict[str, Any]], branch_node_id: int | None) -> list[int]:
|
|
2028
|
+
if branch_node_id is None:
|
|
2029
|
+
return []
|
|
2030
|
+
lane_ids = [
|
|
2031
|
+
node_id
|
|
2032
|
+
for node_id, node in nodes.items()
|
|
2033
|
+
if isinstance(node, dict) and node.get("type") == 2 and node.get("prevId") == int(branch_node_id)
|
|
2034
|
+
]
|
|
2035
|
+
return sorted(lane_ids)
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
def _find_created_branch_node_id(
|
|
2039
|
+
nodes: dict[int, dict[str, Any]],
|
|
2040
|
+
*,
|
|
2041
|
+
before_node_ids: set[int],
|
|
2042
|
+
prev_id: int | None,
|
|
2043
|
+
) -> int | None:
|
|
2044
|
+
candidates = [
|
|
2045
|
+
node_id
|
|
2046
|
+
for node_id, node in nodes.items()
|
|
2047
|
+
if node_id not in before_node_ids and isinstance(node, dict) and node.get("type") == 1
|
|
2048
|
+
]
|
|
2049
|
+
if prev_id is not None:
|
|
2050
|
+
prev_candidates = [node_id for node_id in candidates if nodes[node_id].get("prevId") == int(prev_id)]
|
|
2051
|
+
if prev_candidates:
|
|
2052
|
+
return prev_candidates[0]
|
|
2053
|
+
return candidates[0] if candidates else None
|
|
2054
|
+
|
|
2055
|
+
|
|
2056
|
+
def _find_created_sub_branch_lane_id(
|
|
2057
|
+
nodes: dict[int, dict[str, Any]],
|
|
2058
|
+
*,
|
|
2059
|
+
before_node_ids: set[int],
|
|
2060
|
+
branch_node_id: int | None,
|
|
2061
|
+
) -> int | None:
|
|
2062
|
+
if branch_node_id is None:
|
|
2063
|
+
return None
|
|
2064
|
+
candidates = [
|
|
2065
|
+
node_id
|
|
2066
|
+
for node_id, node in nodes.items()
|
|
2067
|
+
if node_id not in before_node_ids and isinstance(node, dict) and node.get("type") == 2 and node.get("prevId") == int(branch_node_id)
|
|
2068
|
+
]
|
|
2069
|
+
return candidates[0] if candidates else None
|
|
2070
|
+
|
|
2071
|
+
|
|
2072
|
+
def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | None) -> bool:
|
|
2073
|
+
if not isinstance(global_settings, dict):
|
|
2074
|
+
return False
|
|
2075
|
+
for key, value in global_settings.items():
|
|
2076
|
+
if key == "editVersionNo":
|
|
2077
|
+
continue
|
|
2078
|
+
if value is None:
|
|
2079
|
+
continue
|
|
2080
|
+
if isinstance(value, (list, dict)) and not value:
|
|
2081
|
+
continue
|
|
2082
|
+
return True
|
|
2083
|
+
return False
|
|
2084
|
+
|
|
2085
|
+
|
|
2086
|
+
def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
|
|
2087
|
+
try:
|
|
2088
|
+
backend_code = int(error.backend_code)
|
|
2089
|
+
except (TypeError, ValueError):
|
|
2090
|
+
backend_code = None
|
|
2091
|
+
if backend_code != 50004:
|
|
2092
|
+
return False
|
|
2093
|
+
message = error.message or ""
|
|
2094
|
+
return "导航菜单不可用" in message or "插件未安装" in message
|
|
2095
|
+
|
|
2096
|
+
|
|
2097
|
+
def _portal_plan_has_source_type(portal_plan: dict[str, Any], source_type: str) -> bool:
|
|
2098
|
+
components = ((portal_plan.get("update_payload") or {}) if isinstance(portal_plan, dict) else {}).get("components", [])
|
|
2099
|
+
return any(component.get("sourceType") == source_type for component in components if isinstance(component, dict))
|
|
2100
|
+
|
|
2101
|
+
|
|
2102
|
+
def _coerce_qingflow_error(error: Exception) -> QingflowApiError | None:
|
|
2103
|
+
if isinstance(error, QingflowApiError):
|
|
2104
|
+
return error
|
|
2105
|
+
if not isinstance(error, RuntimeError):
|
|
2106
|
+
return None
|
|
2107
|
+
try:
|
|
2108
|
+
payload = json.loads(str(error))
|
|
2109
|
+
except ValueError:
|
|
2110
|
+
return None
|
|
2111
|
+
if not isinstance(payload, dict):
|
|
2112
|
+
return None
|
|
2113
|
+
message = payload.get("message")
|
|
2114
|
+
category = payload.get("category")
|
|
2115
|
+
if not category or not message:
|
|
2116
|
+
return None
|
|
2117
|
+
return QingflowApiError(
|
|
2118
|
+
category=str(category),
|
|
2119
|
+
message=str(message),
|
|
2120
|
+
backend_code=payload.get("backend_code"),
|
|
2121
|
+
request_id=payload.get("request_id"),
|
|
2122
|
+
http_status=payload.get("http_status"),
|
|
2123
|
+
)
|
|
2124
|
+
|
|
2125
|
+
|
|
2126
|
+
def _portal_component_position(
|
|
2127
|
+
source_type: Any,
|
|
2128
|
+
*,
|
|
2129
|
+
pc_x: int,
|
|
2130
|
+
pc_y: int,
|
|
2131
|
+
pc_row_height: int,
|
|
2132
|
+
mobile_y: int,
|
|
2133
|
+
) -> tuple[dict[str, Any], int, int, int, int]:
|
|
2134
|
+
source_name = str(source_type or "").lower()
|
|
2135
|
+
if source_name == "filter":
|
|
2136
|
+
cols = 24
|
|
2137
|
+
rows = 2
|
|
2138
|
+
elif source_name == "grid":
|
|
2139
|
+
cols = 24
|
|
2140
|
+
rows = 4
|
|
2141
|
+
elif source_name == "text":
|
|
2142
|
+
cols = 24
|
|
2143
|
+
rows = 2
|
|
2144
|
+
elif source_name == "view":
|
|
2145
|
+
cols = 24
|
|
2146
|
+
rows = 8
|
|
2147
|
+
elif source_name == "link":
|
|
2148
|
+
cols = 12
|
|
2149
|
+
rows = 2
|
|
2150
|
+
else:
|
|
2151
|
+
cols = 8
|
|
2152
|
+
rows = 4
|
|
2153
|
+
|
|
2154
|
+
if cols == 24:
|
|
2155
|
+
if pc_x != 0:
|
|
2156
|
+
pc_y += pc_row_height
|
|
2157
|
+
pc_x = 0
|
|
2158
|
+
pc_row_height = 0
|
|
2159
|
+
position = {
|
|
2160
|
+
"pc": {"cols": 24, "rows": rows, "x": 0, "y": pc_y},
|
|
2161
|
+
"mobile": {"cols": 6, "rows": rows, "x": 0, "y": mobile_y},
|
|
2162
|
+
}
|
|
2163
|
+
pc_y += rows
|
|
2164
|
+
mobile_y += rows
|
|
2165
|
+
return position, 0, pc_y, 0, mobile_y
|
|
2166
|
+
|
|
2167
|
+
if pc_x + cols > 24:
|
|
2168
|
+
pc_y += pc_row_height
|
|
2169
|
+
pc_x = 0
|
|
2170
|
+
pc_row_height = 0
|
|
2171
|
+
|
|
2172
|
+
position = {
|
|
2173
|
+
"pc": {"cols": cols, "rows": rows, "x": pc_x, "y": pc_y},
|
|
2174
|
+
"mobile": {"cols": 6, "rows": rows, "x": 0, "y": mobile_y},
|
|
2175
|
+
}
|
|
2176
|
+
pc_x += cols
|
|
2177
|
+
pc_row_height = max(pc_row_height, rows)
|
|
2178
|
+
if pc_x >= 24:
|
|
2179
|
+
pc_y += pc_row_height
|
|
2180
|
+
pc_x = 0
|
|
2181
|
+
pc_row_height = 0
|
|
2182
|
+
mobile_y += rows
|
|
2183
|
+
return position, pc_x, pc_y, pc_row_height, mobile_y
|
|
2184
|
+
|
|
2185
|
+
|
|
2186
|
+
def _portal_view_type(store: RunArtifactStore, entity_id: str, view_id: str) -> str:
|
|
2187
|
+
view_type_map = {
|
|
2188
|
+
"table": "tableView",
|
|
2189
|
+
"card": "cardView",
|
|
2190
|
+
"board": "boardView",
|
|
2191
|
+
"gantt": "ganttView",
|
|
2192
|
+
"hierarchy": "hierarchyView",
|
|
2193
|
+
}
|
|
2194
|
+
for entity in store.data.get("normalized_solution_spec", {}).get("entities", []):
|
|
2195
|
+
if entity["entity_id"] != entity_id:
|
|
2196
|
+
continue
|
|
2197
|
+
for view in entity.get("views", []):
|
|
2198
|
+
if view.get("view_id") == view_id:
|
|
2199
|
+
return view_type_map.get(view.get("type"), "tableView")
|
|
2200
|
+
return "tableView"
|
|
2201
|
+
|
|
2202
|
+
|
|
2203
|
+
def _coerce_nested_error_payload(exc: Exception) -> dict[str, Any]:
|
|
2204
|
+
if isinstance(exc, QingflowApiError):
|
|
2205
|
+
return exc.to_dict()
|
|
2206
|
+
text = str(exc)
|
|
2207
|
+
try:
|
|
2208
|
+
parsed = json.loads(text)
|
|
2209
|
+
except Exception:
|
|
2210
|
+
return {"message": text}
|
|
2211
|
+
if isinstance(parsed, dict):
|
|
2212
|
+
return parsed
|
|
2213
|
+
return {"message": text}
|
|
2214
|
+
|
|
2215
|
+
|
|
2216
|
+
def _compiled_field_catalog(entity: CompiledEntity) -> list[dict[str, Any]]:
|
|
2217
|
+
catalog: list[dict[str, Any]] = []
|
|
2218
|
+
for field_id, field_spec in entity.field_specs.items():
|
|
2219
|
+
if not isinstance(field_spec, dict):
|
|
2220
|
+
continue
|
|
2221
|
+
catalog.append(
|
|
2222
|
+
{
|
|
2223
|
+
"field_id": field_id,
|
|
2224
|
+
"label": field_spec.get("label"),
|
|
2225
|
+
"type": field_spec.get("type"),
|
|
2226
|
+
"required": field_spec.get("required"),
|
|
2227
|
+
"target_entity_id": field_spec.get("target_entity_id"),
|
|
2228
|
+
}
|
|
2229
|
+
)
|
|
2230
|
+
return catalog
|
|
2231
|
+
|
|
2232
|
+
|
|
2233
|
+
def _summarize_form_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
2234
|
+
rows = payload.get("formQues")
|
|
2235
|
+
question_titles: list[str] = []
|
|
2236
|
+
question_type_counts: dict[str, int] = {}
|
|
2237
|
+
if isinstance(rows, list):
|
|
2238
|
+
for row in rows:
|
|
2239
|
+
for question in _flatten_questions(row):
|
|
2240
|
+
title = question.get("queTitle")
|
|
2241
|
+
if isinstance(title, str) and title:
|
|
2242
|
+
question_titles.append(title)
|
|
2243
|
+
que_type = question.get("queType")
|
|
2244
|
+
if que_type is not None:
|
|
2245
|
+
key = str(que_type)
|
|
2246
|
+
question_type_counts[key] = question_type_counts.get(key, 0) + 1
|
|
2247
|
+
return {
|
|
2248
|
+
"form_title": payload.get("formTitle"),
|
|
2249
|
+
"edit_version_no": payload.get("editVersionNo"),
|
|
2250
|
+
"question_row_count": len(rows) if isinstance(rows, list) else 0,
|
|
2251
|
+
"question_titles": question_titles,
|
|
2252
|
+
"question_type_counts": question_type_counts,
|
|
2253
|
+
"has_question_relations": bool(payload.get("questionRelations")),
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
|
|
2257
|
+
def _flatten_questions(value: Any) -> list[dict[str, Any]]:
|
|
2258
|
+
if isinstance(value, dict):
|
|
2259
|
+
nested = [value]
|
|
2260
|
+
for key in ("innerQuestions", "subQuestions"):
|
|
2261
|
+
children = value.get(key)
|
|
2262
|
+
if isinstance(children, list):
|
|
2263
|
+
for item in children:
|
|
2264
|
+
nested.extend(_flatten_questions(item))
|
|
2265
|
+
return nested
|
|
2266
|
+
if isinstance(value, list):
|
|
2267
|
+
flattened: list[dict[str, Any]] = []
|
|
2268
|
+
for item in value:
|
|
2269
|
+
flattened.extend(_flatten_questions(item))
|
|
2270
|
+
return flattened
|
|
2271
|
+
return []
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
def _package_item_app_key(item: dict[str, Any]) -> str | None:
|
|
2275
|
+
item_type = item.get("itemType")
|
|
2276
|
+
if item_type is not None and item_type not in {PACKAGE_ITEM_TYPE_FORM, str(PACKAGE_ITEM_TYPE_FORM)}:
|
|
2277
|
+
return None
|
|
2278
|
+
app_key = item.get("appKey")
|
|
2279
|
+
return app_key if isinstance(app_key, str) and app_key else None
|
|
2280
|
+
|
|
2281
|
+
|
|
2282
|
+
def _verify_package_attachment(package_tools: PackageTools, *, profile: str, tag_id: int, app_key: str, attempts: int = 2) -> dict[str, Any]:
|
|
2283
|
+
last_detail: dict[str, Any] = {"result": {}}
|
|
2284
|
+
for _ in range(max(attempts, 1)):
|
|
2285
|
+
last_detail = package_tools.package_get(profile=profile, tag_id=tag_id, include_raw=True)
|
|
2286
|
+
result = last_detail.get("result") if isinstance(last_detail.get("result"), dict) else {}
|
|
2287
|
+
verified_items = [deepcopy(existing) for existing in result.get("tagItems", []) if isinstance(existing, dict)]
|
|
2288
|
+
if any(_package_item_app_key(existing) == app_key for existing in verified_items):
|
|
2289
|
+
return last_detail
|
|
2290
|
+
return last_detail
|
|
2291
|
+
|
|
2292
|
+
|
|
2293
|
+
def _resolve_package_item_insert_index(tag_items: list[dict[str, Any]], ordinal: Any) -> int:
|
|
2294
|
+
if not isinstance(ordinal, int) or ordinal <= 0:
|
|
2295
|
+
return len(tag_items)
|
|
2296
|
+
return max(0, min(len(tag_items), ordinal - 1))
|
|
2297
|
+
|
|
2298
|
+
|
|
2299
|
+
def _build_viewgraph_questions(schema: dict[str, Any], visible_que_ids: list[int]) -> list[dict[str, Any]]:
|
|
2300
|
+
visible_map = {que_id: index for index, que_id in enumerate(visible_que_ids, start=1)}
|
|
2301
|
+
questions: list[dict[str, Any]] = []
|
|
2302
|
+
for base_question in schema.get("baseQues", []):
|
|
2303
|
+
question = _build_viewgraph_question(base_question, visible_map, default_auth=3)
|
|
2304
|
+
if question is not None:
|
|
2305
|
+
questions.append(question)
|
|
2306
|
+
for line in schema.get("formQues", []):
|
|
2307
|
+
for question in line:
|
|
2308
|
+
resolved = _build_viewgraph_question(question, visible_map, default_auth=1)
|
|
2309
|
+
if resolved is not None:
|
|
2310
|
+
questions.append(resolved)
|
|
2311
|
+
return questions
|
|
2312
|
+
|
|
2313
|
+
|
|
2314
|
+
def _build_viewgraph_question(question: dict[str, Any], visible_map: dict[int, int], *, default_auth: int) -> dict[str, Any] | None:
|
|
2315
|
+
que_id = question.get("queId")
|
|
2316
|
+
if que_id is None:
|
|
2317
|
+
return None
|
|
2318
|
+
sub_questions = [
|
|
2319
|
+
resolved
|
|
2320
|
+
for sub_question in question.get("subQuestions", []) or []
|
|
2321
|
+
if (resolved := _build_viewgraph_question(sub_question, visible_map, default_auth=1)) is not None
|
|
2322
|
+
]
|
|
2323
|
+
inner_questions = [
|
|
2324
|
+
resolved
|
|
2325
|
+
for inner_row in question.get("innerQuestions", []) or []
|
|
2326
|
+
for inner_question in inner_row or []
|
|
2327
|
+
if (resolved := _build_viewgraph_question(inner_question, visible_map, default_auth=1)) is not None
|
|
2328
|
+
]
|
|
2329
|
+
return {
|
|
2330
|
+
"queId": que_id,
|
|
2331
|
+
"queTitle": question.get("queTitle"),
|
|
2332
|
+
"queType": question.get("queType"),
|
|
2333
|
+
"queAuth": default_auth if que_id <= 5 else 1,
|
|
2334
|
+
"beingListDisplay": que_id in visible_map,
|
|
2335
|
+
"displayOrdinal": visible_map.get(que_id, -1),
|
|
2336
|
+
"subQues": sub_questions,
|
|
2337
|
+
"innerQues": inner_questions,
|
|
2338
|
+
"beingDownload": True,
|
|
2339
|
+
}
|