feat(pbl_validation): 初始提交 - pbls 项目模块代码入库
This commit is contained in:
parent
0dd6461a0f
commit
4cf0a3d04e
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
.DS_Store
|
||||
*.log
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.venv/
|
||||
venv/
|
||||
174
json/validation_pbl_validation_result.json
Normal file
174
json/validation_pbl_validation_result.json
Normal file
@ -0,0 +1,174 @@
|
||||
{
|
||||
"tblname": "pbl_validation_result",
|
||||
"alias": "pbl_validation",
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"blueprint_id": {
|
||||
"label": "蓝图ID",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"blueprint_version_no": {
|
||||
"label": "校验时版本",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"dimension_count": {
|
||||
"label": "维度数(=14)",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"passed_count": {
|
||||
"label": "通过数",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"failed_count": {
|
||||
"label": "失败数",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"score": {
|
||||
"label": "得分",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"quality_state": {
|
||||
"label": "质量状态",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"findings_json": {
|
||||
"label": "逐维结论",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"rule_set_version": {
|
||||
"label": "规则集版本",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"created_by": {
|
||||
"label": "触发人",
|
||||
"type": "text",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"blueprint_id": {
|
||||
"label": "蓝图ID",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"blueprint_version_no": {
|
||||
"label": "校验时版本",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"dimension_count": {
|
||||
"label": "维度数(=14)",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"passed_count": {
|
||||
"label": "通过数",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"failed_count": {
|
||||
"label": "失败数",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"score": {
|
||||
"label": "得分",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"quality_state": {
|
||||
"label": "质量状态",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"findings_json": {
|
||||
"label": "逐维结论",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"rule_set_version": {
|
||||
"label": "规则集版本",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"created_by": {
|
||||
"label": "触发人",
|
||||
"type": "text",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"blueprint_id": {
|
||||
"label": "蓝图ID",
|
||||
"type": "number"
|
||||
},
|
||||
"blueprint_version_no": {
|
||||
"label": "校验时版本",
|
||||
"type": "number"
|
||||
},
|
||||
"dimension_count": {
|
||||
"label": "维度数(=14)",
|
||||
"type": "number"
|
||||
},
|
||||
"passed_count": {
|
||||
"label": "通过数",
|
||||
"type": "number"
|
||||
},
|
||||
"failed_count": {
|
||||
"label": "失败数",
|
||||
"type": "number"
|
||||
},
|
||||
"score": {
|
||||
"label": "得分",
|
||||
"type": "number"
|
||||
},
|
||||
"quality_state": {
|
||||
"label": "质量状态",
|
||||
"type": "select"
|
||||
},
|
||||
"findings_json": {
|
||||
"label": "逐维结论",
|
||||
"type": "text"
|
||||
},
|
||||
"rule_set_version": {
|
||||
"label": "规则集版本",
|
||||
"type": "select"
|
||||
},
|
||||
"created_by": {
|
||||
"label": "触发人",
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
"order_by": "id DESC",
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
146
json/validation_pbl_validation_rule.json
Normal file
146
json/validation_pbl_validation_rule.json
Normal file
@ -0,0 +1,146 @@
|
||||
{
|
||||
"tblname": "pbl_validation_rule",
|
||||
"alias": "pbl_validation",
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"code": {
|
||||
"label": "规则编码",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"dimension": {
|
||||
"label": "维度",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"title": {
|
||||
"label": "标题",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"rule_json": {
|
||||
"label": "规则体",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"threshold_json": {
|
||||
"label": "阈值(可配,Q-OPEN-8)",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"severity": {
|
||||
"label": "严重度",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"enabled": {
|
||||
"label": "启用",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"code": {
|
||||
"label": "规则编码",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"dimension": {
|
||||
"label": "维度",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"title": {
|
||||
"label": "标题",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"rule_json": {
|
||||
"label": "规则体",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"threshold_json": {
|
||||
"label": "阈值(可配,Q-OPEN-8)",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"severity": {
|
||||
"label": "严重度",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"enabled": {
|
||||
"label": "启用",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"code": {
|
||||
"label": "规则编码",
|
||||
"type": "select"
|
||||
},
|
||||
"dimension": {
|
||||
"label": "维度",
|
||||
"type": "select"
|
||||
},
|
||||
"title": {
|
||||
"label": "标题",
|
||||
"type": "select"
|
||||
},
|
||||
"rule_json": {
|
||||
"label": "规则体",
|
||||
"type": "text"
|
||||
},
|
||||
"threshold_json": {
|
||||
"label": "阈值(可配,Q-OPEN-8)",
|
||||
"type": "text"
|
||||
},
|
||||
"severity": {
|
||||
"label": "严重度",
|
||||
"type": "select"
|
||||
},
|
||||
"enabled": {
|
||||
"label": "启用",
|
||||
"type": "checkbox"
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"order_by": "id DESC",
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
125
models/pbl_validation_result.json
Normal file
125
models/pbl_validation_result.json
Normal file
@ -0,0 +1,125 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pbl_validation_result",
|
||||
"comment": "校验结果(14 维 + 5 级质量状态)",
|
||||
"module": "pbl_validation",
|
||||
"engine": "mariadb",
|
||||
"charset": "utf8mb4",
|
||||
"tenant_scoped": true
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"comment": "租户ID(强制打头)",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"comment": "主键",
|
||||
"null": false,
|
||||
"type": "int",
|
||||
"length": 20,
|
||||
"unsigned": true,
|
||||
"auto_increment": true
|
||||
},
|
||||
{
|
||||
"name": "blueprint_id",
|
||||
"comment": "蓝图ID",
|
||||
"null": false,
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"name": "blueprint_version_no",
|
||||
"comment": "校验时版本",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "dimension_count",
|
||||
"comment": "维度数(=14)",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "passed_count",
|
||||
"comment": "通过数",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "failed_count",
|
||||
"comment": "失败数",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "score",
|
||||
"comment": "得分",
|
||||
"null": false,
|
||||
"type": "double",
|
||||
"precision": 18,
|
||||
"scale": 2
|
||||
},
|
||||
{
|
||||
"name": "quality_state",
|
||||
"comment": "质量状态",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "findings_json",
|
||||
"comment": "逐维结论",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "rule_set_version",
|
||||
"comment": "规则集版本",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "created_by",
|
||||
"comment": "触发人",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"comment": "创建时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"comment": "更新时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_vres_bp",
|
||||
"unique": false,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"blueprint_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PRIMARY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
113
models/pbl_validation_rule.json
Normal file
113
models/pbl_validation_rule.json
Normal file
@ -0,0 +1,113 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pbl_validation_rule",
|
||||
"comment": "校验规则(14 维,规则 JSON 形态 script_type=1)",
|
||||
"module": "pbl_validation",
|
||||
"engine": "mariadb",
|
||||
"charset": "utf8mb4",
|
||||
"tenant_scoped": true
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"comment": "租户ID(强制打头)",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"comment": "主键",
|
||||
"null": false,
|
||||
"type": "int",
|
||||
"length": 20,
|
||||
"unsigned": true,
|
||||
"auto_increment": true
|
||||
},
|
||||
{
|
||||
"name": "code",
|
||||
"comment": "规则编码",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "dimension",
|
||||
"comment": "维度",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"comment": "标题",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 128
|
||||
},
|
||||
{
|
||||
"name": "rule_json",
|
||||
"comment": "规则体",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "threshold_json",
|
||||
"comment": "阈值(可配,Q-OPEN-8)",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "severity",
|
||||
"comment": "严重度",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"comment": "启用",
|
||||
"null": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "version_no",
|
||||
"comment": "版本",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"comment": "创建时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"comment": "更新时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "uk_vr_code",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"code",
|
||||
"version_no"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PRIMARY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
25
pbl_validation/__init__.py
Normal file
25
pbl_validation/__init__.py
Normal file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_validation —— PBL 校验引擎(14 维 + 5 级质量状态,M2)
|
||||
|
||||
注册三处同步之 ②:必须导出 init.py 里的全部契约函数,漏一行 .dspy 调用即 NameError。
|
||||
"""
|
||||
from pbl_validation.init import load_pbl_validation
|
||||
from pbl_validation.api import (
|
||||
pbl_validation_run,
|
||||
pbl_validation_get,
|
||||
pbl_validation_list,
|
||||
pbl_validation_rule_list,
|
||||
pbl_validation_rule_save,
|
||||
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'load_pbl_validation',
|
||||
'pbl_validation_run',
|
||||
'pbl_validation_get',
|
||||
'pbl_validation_list',
|
||||
'pbl_validation_rule_list',
|
||||
'pbl_validation_rule_save',
|
||||
|
||||
]
|
||||
237
pbl_validation/api.py
Normal file
237
pbl_validation/api.py
Normal file
@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_validation.api —— 14 维校验引擎 + 5 级 quality_state(第15章,M2)。
|
||||
|
||||
规则形态:规则体存 `pbl_validation_rule.rule_json`(与 script_engine script_type=1 的
|
||||
event→condition→response 同构,**不新建规则引擎**),阈值可配(Q-OPEN-8)。
|
||||
缺规则时用内置 DEFAULT_RULES 兜底,保证校验链路永远可跑(fail-closed:无结论=不通过)。
|
||||
"""
|
||||
import json
|
||||
|
||||
from pbl_common.api import (PblError, actor_id, crud, json_dump, now_str, sql_exec,
|
||||
sql_rows, sql_scalar, tenant_id)
|
||||
|
||||
DIMENSIONS = [
|
||||
'learning_goal_clarity', 'driving_question_quality', 'task_alignment',
|
||||
'role_meaningfulness', 'artifact_authenticity', 'evidence_sufficiency',
|
||||
'assessment_alignment', 'collaboration_design', 'resource_adequacy',
|
||||
'constraint_feasibility', 'reflection_design', 'difficulty_gradient',
|
||||
'engagement_potential', 'cross_discipline_value',
|
||||
]
|
||||
# 质量状态阶梯(第15章 5 级):逐级门禁,不可跳级
|
||||
LADDER = ['draft', 'needs_work', 'pbl_ready', 'playtest_ready', 'publish_ready']
|
||||
GATES = {
|
||||
'pbl_ready': {'min_passed': 10, 'max_blocking': 0, 'min_score': 60},
|
||||
'playtest_ready': {'min_passed': 12, 'max_blocking': 0, 'min_score': 75},
|
||||
'publish_ready': {'min_passed': 14, 'max_blocking': 0, 'min_score': 85},
|
||||
}
|
||||
SEVERITY_BLOCKING = ('blocking', 'critical')
|
||||
|
||||
DEFAULT_RULES = {
|
||||
'learning_goal_clarity': {'op': 'min_count', 'path': 'learning_goals', 'value': 2,
|
||||
'severity': 'blocking'},
|
||||
'driving_question_quality': {'op': 'min_text_len', 'path': 'driving_question', 'value': 10,
|
||||
'severity': 'blocking'},
|
||||
'task_alignment': {'op': 'min_count', 'path': 'missions', 'value': 1, 'severity': 'blocking'},
|
||||
'role_meaningfulness': {'op': 'min_count', 'path': 'roles', 'value': 2, 'severity': 'warning'},
|
||||
'artifact_authenticity': {'op': 'min_count', 'path': 'artifacts', 'value': 1,
|
||||
'severity': 'blocking'},
|
||||
'evidence_sufficiency': {'op': 'min_count', 'path': 'evidence_specs', 'value': 2,
|
||||
'severity': 'warning'},
|
||||
'assessment_alignment': {'op': 'goal_coverage', 'value': 0.6, 'severity': 'blocking'},
|
||||
'collaboration_design': {'op': 'min_count', 'path': 'collaboration', 'value': 1,
|
||||
'severity': 'warning'},
|
||||
'resource_adequacy': {'op': 'min_count', 'path': 'resources', 'value': 1, 'severity': 'warning'},
|
||||
'constraint_feasibility': {'op': 'min_count', 'path': 'constraints', 'value': 0,
|
||||
'severity': 'info'},
|
||||
'reflection_design': {'op': 'min_count', 'path': 'reflections', 'value': 1,
|
||||
'severity': 'warning'},
|
||||
'difficulty_gradient': {'op': 'distinct_values', 'path': 'tasks.difficulty', 'value': 2,
|
||||
'severity': 'info'},
|
||||
'engagement_potential': {'op': 'min_text_len', 'path': 'scenario', 'value': 8,
|
||||
'severity': 'warning'},
|
||||
'cross_discipline_value': {'op': 'min_count', 'path': 'learning_goals', 'value': 3,
|
||||
'severity': 'info'},
|
||||
}
|
||||
|
||||
|
||||
async def _tid():
|
||||
return await tenant_id()
|
||||
|
||||
|
||||
def _loads(v, default=None):
|
||||
if isinstance(v, (dict, list)):
|
||||
return v
|
||||
try:
|
||||
return json.loads(v) if v else (default if default is not None else {})
|
||||
except ValueError:
|
||||
return default if default is not None else {}
|
||||
|
||||
|
||||
def _path_get(model, path):
|
||||
cur = model
|
||||
for seg in path.split('.'):
|
||||
if isinstance(cur, list):
|
||||
cur = [x.get(seg) for x in cur if isinstance(x, dict)]
|
||||
elif isinstance(cur, dict):
|
||||
cur = cur.get(seg)
|
||||
else:
|
||||
return None
|
||||
return cur
|
||||
|
||||
|
||||
def _check(model, rule):
|
||||
op, path, want = rule.get('op'), rule.get('path'), rule.get('value', 0)
|
||||
got = _path_get(model, path or '')
|
||||
if op == 'min_count':
|
||||
n = len(got or []) if isinstance(got, (list, dict)) else (0 if got is None else 1)
|
||||
return n >= int(want), 'count=%s need>=%s' % (n, want)
|
||||
if op == 'min_text_len':
|
||||
s = got if isinstance(got, str) else (got[0] if isinstance(got, list) and got else '')
|
||||
s = s or ''
|
||||
return len(s) >= int(want), 'len=%s need>=%s' % (len(s), want)
|
||||
if op == 'goal_coverage':
|
||||
goals = [g.get('uid') or g.get('code') for g in (model.get('learning_goals') or [])]
|
||||
crits = [c.get('learning_goal_id') or c.get('learning_goal_uid')
|
||||
for c in (model.get('rubric_criteria') or [])]
|
||||
cov = len({g for g in goals if g in crits})
|
||||
ratio = (cov / len(goals)) if goals else 0.0
|
||||
return ratio >= float(want), 'coverage=%.2f need>=%s' % (ratio, want)
|
||||
if op == 'distinct_values':
|
||||
vals = set()
|
||||
for item in (got or []):
|
||||
vals.add(item if not isinstance(item, dict) else json.dumps(item, sort_keys=True))
|
||||
return len(vals) >= int(want), 'distinct=%s need>=%s' % (len(vals), want)
|
||||
return False, 'unknown_op:%s' % op
|
||||
|
||||
|
||||
async def _rules(tid):
|
||||
rows = await sql_rows('SELECT `dimension`,`rule_json`,`threshold_json`,`severity`'
|
||||
' FROM `pbl_validation_rule` WHERE `tenant_id` = ${t}$'
|
||||
' AND `enabled` = 1 ORDER BY `version_no` DESC', {'t': tid}, 'pbl')
|
||||
out, seen = {}, set()
|
||||
for r in rows:
|
||||
if r['dimension'] in seen:
|
||||
continue
|
||||
seen.add(r['dimension'])
|
||||
body = _loads(r['rule_json'], {})
|
||||
body['severity'] = r['severity'] or body.get('severity', 'warning')
|
||||
thr = _loads(r['threshold_json'], {})
|
||||
if 'value' in thr:
|
||||
body['value'] = thr['value']
|
||||
out[r['dimension']] = body
|
||||
for dim, body in DEFAULT_RULES.items():
|
||||
out.setdefault(dim, dict(body))
|
||||
return out
|
||||
|
||||
|
||||
async def pbl_validation_run(**kw):
|
||||
"""执行 14 维校验并落 pbl_validation_result;返回逐维可解释结论。"""
|
||||
import time
|
||||
t0 = time.time()
|
||||
tid = await _tid()
|
||||
bp_id = kw.get('blueprint_id')
|
||||
bps = await sql_rows('SELECT * FROM `pbl_blueprint` WHERE `tenant_id` = ${t}$'
|
||||
' AND `id` = ${id}$ LIMIT 1', {'t': tid, 'id': bp_id}, 'pbl')
|
||||
if not bps:
|
||||
raise PblError('PBL_NOT_FOUND', '蓝图不存在')
|
||||
bp = bps[0]
|
||||
model = _loads(bp['content_json'])
|
||||
for key, table in (('learning_goals', 'pbl_blueprint_learning_goal'),
|
||||
('roles', 'pbl_blueprint_role'), ('missions', 'pbl_blueprint_mission'),
|
||||
('tasks', 'pbl_blueprint_task'), ('artifacts', 'pbl_blueprint_artifact_spec'),
|
||||
('evidence_specs', 'pbl_blueprint_evidence_spec'),
|
||||
('reflections', 'pbl_blueprint_reflection_spec')):
|
||||
rows = await sql_rows('SELECT * FROM `%s` WHERE `tenant_id` = ${t}$'
|
||||
' AND `blueprint_id` = ${b}$ ORDER BY `order_no`' % table,
|
||||
{'t': tid, 'b': bp_id}, 'pbl')
|
||||
if rows:
|
||||
model[key] = rows
|
||||
rules = await _rules(tid)
|
||||
findings, passed, blocking = [], 0, 0
|
||||
for dim in DIMENSIONS:
|
||||
rule = rules.get(dim) or {'op': 'min_count', 'path': dim, 'value': 1, 'severity': 'warning'}
|
||||
ok, detail = _check(model, rule)
|
||||
sev = rule.get('severity', 'warning')
|
||||
if ok:
|
||||
passed += 1
|
||||
elif sev in SEVERITY_BLOCKING:
|
||||
blocking += 1
|
||||
findings.append({'dimension': dim, 'passed': bool(ok), 'severity': sev,
|
||||
'detail': detail, 'rule_value': rule.get('value'),
|
||||
'recommendation': None if ok else '补充 %s(%s)' % (dim, detail)})
|
||||
total = len(DIMENSIONS)
|
||||
score = round(100.0 * passed / total, 2)
|
||||
state = _grade(score, passed, blocking)
|
||||
await sql_exec(
|
||||
'INSERT INTO `pbl_validation_result` (`tenant_id`,`blueprint_id`,`blueprint_version_no`,'
|
||||
'`dimension_count`,`passed_count`,`failed_count`,`score`,`quality_state`,'
|
||||
'`findings_json`,`rule_set_version`,`created_by`,`created_at`)'
|
||||
' VALUES (${t}$,${b}$,${v}$,${dc}$,${pc}$,${fc}$,${s}$,${q}$,${f}$,${r}$,${u}$,${ts}$)',
|
||||
{'t': tid, 'b': bp_id, 'v': bp['version_no'], 'dc': total, 'pc': passed,
|
||||
'fc': total - passed, 's': score, 'q': state, 'f': json_dump(findings),
|
||||
'r': 'pbl-validation-v1', 'u': await actor_id(), 'ts': now_str()}, 'pbl')
|
||||
if kw.get('apply_state'):
|
||||
await sql_exec('UPDATE `pbl_blueprint` SET `quality_state` = ${q}$,'
|
||||
' `updated_at` = ${ts}$ WHERE `id` = ${id}$ AND `tenant_id` = ${t}$',
|
||||
{'q': state, 'ts': now_str(), 'id': bp_id, 't': tid}, 'pbl')
|
||||
return {'ok': True, 'blueprint_id': bp_id, 'dimension_count': total, 'passed': passed,
|
||||
'failed': total - passed, 'blocking': blocking, 'score': score,
|
||||
'quality_state': state, 'findings': findings,
|
||||
'latency_ms': int((time.time() - t0) * 1000),
|
||||
'budget_ms': 2000, 'within_budget': (time.time() - t0) * 1000 <= 2000}
|
||||
|
||||
|
||||
def _grade(score, passed, blocking):
|
||||
best = 'draft'
|
||||
for state in LADDER[2:]:
|
||||
g = GATES[state]
|
||||
if blocking > g['max_blocking'] or passed < g['min_passed'] or score < g['min_score']:
|
||||
break
|
||||
best = state
|
||||
if blocking:
|
||||
return 'needs_work' if score >= 40 else 'draft'
|
||||
return best
|
||||
|
||||
|
||||
async def pbl_validation_get(**kw):
|
||||
tid = await _tid()
|
||||
rows = await sql_rows('SELECT * FROM `pbl_validation_result` WHERE `tenant_id` = ${t}$'
|
||||
' AND `id` = ${id}$ LIMIT 1', {'t': tid, 'id': kw.get('id')}, 'pbl')
|
||||
if not rows:
|
||||
raise PblError('PBL_NOT_FOUND', '校验结果不存在')
|
||||
rows[0]['findings_json'] = _loads(rows[0].get('findings_json'), [])
|
||||
return {'ok': True, 'data': rows[0]}
|
||||
|
||||
|
||||
async def pbl_validation_list(**kw):
|
||||
"""校验结果分页(只读,tenant 强制)。"""
|
||||
sc = crud('pbl_validation_result', 'pbl',
|
||||
['blueprint_id', 'blueprint_version_no', 'dimension_count', 'passed_count',
|
||||
'failed_count', 'score', 'quality_state', 'rule_set_version', 'created_by'])
|
||||
return await sc['list'](**kw)
|
||||
|
||||
|
||||
async def pbl_validation_rule_list(**kw):
|
||||
tid = await _tid()
|
||||
rows = await sql_rows('SELECT * FROM `pbl_validation_rule` WHERE `tenant_id` = ${t}$'
|
||||
' ORDER BY `dimension`,`version_no` DESC', {'t': tid}, 'pbl')
|
||||
return {'ok': True, 'data': rows, 'dimensions': DIMENSIONS,
|
||||
'builtin_defaults': DEFAULT_RULES if kw.get('with_defaults') else None}
|
||||
|
||||
|
||||
async def pbl_validation_rule_save(**kw):
|
||||
dim = kw.get('dimension')
|
||||
if dim not in DIMENSIONS:
|
||||
raise PblError('PBL_DIMENSION_UNKNOWN', 'dimension 须属于 14 维之一')
|
||||
sc = crud('pbl_validation_rule', 'pbl', ['code', 'dimension', 'title', 'rule_json',
|
||||
'threshold_json', 'severity', 'enabled',
|
||||
'version_no'])
|
||||
payload = dict(kw)
|
||||
payload['rule_json'] = json_dump(kw.get('rule_json'))
|
||||
payload['threshold_json'] = json_dump(kw.get('threshold_json'))
|
||||
payload.setdefault('version_no', 1)
|
||||
payload.setdefault('code', 'VR-%s' % dim)
|
||||
if kw.get('id'):
|
||||
return await sc['update'](**payload)
|
||||
return await sc['create'](**payload)
|
||||
27
pbl_validation/init.py
Normal file
27
pbl_validation/init.py
Normal file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""`load_pbl_validation()` —— pbl_validation 模块唯一挂载入口。
|
||||
|
||||
注册三处同步之 ③:env.<契约名> = <契约名>(① 定义在 api.py,② 导出在 __init__.py)。
|
||||
"""
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
from pbl_validation.api import (
|
||||
pbl_validation_run,
|
||||
pbl_validation_get,
|
||||
pbl_validation_list,
|
||||
pbl_validation_rule_list,
|
||||
pbl_validation_rule_save,
|
||||
|
||||
)
|
||||
|
||||
|
||||
def load_pbl_validation():
|
||||
env = ServerEnv()
|
||||
env.pbl_validation_run = pbl_validation_run
|
||||
env.pbl_validation_get = pbl_validation_get
|
||||
env.pbl_validation_list = pbl_validation_list
|
||||
env.pbl_validation_rule_list = pbl_validation_rule_list
|
||||
env.pbl_validation_rule_save = pbl_validation_rule_save
|
||||
|
||||
return 'pbl_validation'
|
||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "pbl_validation"
|
||||
version = "0.1.0"
|
||||
description = "PBL 校验引擎(14 维 + 5 级质量状态,M2)"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["pbl_validation"]
|
||||
44
scripts/load_path.py
Normal file
44
scripts/load_path.py
Normal file
@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_validation RBAC 路径注册(硬门禁 6.6 / QC #11)。
|
||||
|
||||
约定:
|
||||
- 路径 = 模块自动路由 `/pbl_validation/api/<契约>.dspy`,不带端口、不带 /wss 前缀;
|
||||
- 角色 `logined` = 登录即可访问的读接口;写接口按角色分级(teacher/admin);
|
||||
- 由 apps/pbls/build.sh 第 8 步调用 `register()`;rbac CLI 不在位时打印清单(不静默跳过)。
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
MODULE = 'pbl_validation'
|
||||
|
||||
# (path, role)
|
||||
PATHS = [
|
||||
('/pbl_validation/api/pbl_validation_run.dspy', 'logined'),
|
||||
('/pbl_validation/api/pbl_validation_get.dspy', 'logined'),
|
||||
('/pbl_validation/api/pbl_validation_list.dspy', 'logined'),
|
||||
('/pbl_validation/api/pbl_validation_rule_list.dspy', 'logined'),
|
||||
('/pbl_validation/api/pbl_validation_rule_save.dspy', 'logined'),
|
||||
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py')
|
||||
done, missing = 0, []
|
||||
for path, role in PATHS:
|
||||
if subprocess.call([sys.executable if os.environ.get('PY') else 'python3',
|
||||
tool, role, path],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
|
||||
done += 1
|
||||
else:
|
||||
missing.append((path, role))
|
||||
print('[%s] rbac paths: total=%%d ok=%%d pending=%%d' %% (len(PATHS), done, len(missing)))
|
||||
for path, role in missing:
|
||||
print(' PENDING %%-12s %%s' %% (role, path))
|
||||
return len(missing) == 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(0 if register() else 1)
|
||||
24
skill/SKILL.md
Normal file
24
skill/SKILL.md
Normal file
@ -0,0 +1,24 @@
|
||||
# pbl_validation 模块技能(自动生成骨架 + 人工补充)
|
||||
|
||||
## 定位
|
||||
PBL 校验引擎(14 维 + 5 级质量状态,M2)
|
||||
|
||||
## 挂载
|
||||
`from pbl_validation.init import load_pbl_validation` → `load_pbl_validation()`(应用 app/pbls.py init() 中按序调用)
|
||||
|
||||
## 数据表(2 张)
|
||||
- `pbl_validation_rule`:校验规则(14 维,规则 JSON 形态 script_type=1)
|
||||
- `pbl_validation_result`:校验结果(14 维 + 5 级质量状态)
|
||||
|
||||
## 契约接口(5 个,路径 `/pbl_validation/api/<name>.dspy`)
|
||||
- `pbl_validation_run`
|
||||
- `pbl_validation_get`
|
||||
- `pbl_validation_list`
|
||||
- `pbl_validation_rule_list`
|
||||
- `pbl_validation_rule_save`
|
||||
|
||||
## 陷阱
|
||||
- 库名一律 `ServerEnv().get_module_dbname('pbl_validation')`,禁止硬编码 DBNAME。
|
||||
- sqlor 只有 `C/U/D/R/I/sqlExe`;查询走 pbl_common.api 的 q_all/q_one(已适配)。
|
||||
- 所有读写强制带 `tenant_id`(pbl_common.api.tenant_id()),缺失即 fail-closed 报错。
|
||||
- 新增契约需同步三处:api.py 定义 + __init__.py 导出 + init.py env 注册 + scripts/load_path.py 路径。
|
||||
37
sql/pbl_validation.sql
Normal file
37
sql/pbl_validation.sql
Normal file
@ -0,0 +1,37 @@
|
||||
-- pbl_validation 表 DDL(自动生成,与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)
|
||||
CREATE TABLE IF NOT EXISTS `pbl_validation_rule` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "规则编码",
|
||||
`dimension` VARCHAR(64) NOT NULL COMMENT "维度",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`rule_json` LONGTEXT NULL COMMENT "规则体",
|
||||
`threshold_json` LONGTEXT NULL COMMENT "阈值(可配,Q-OPEN-8)",
|
||||
`severity` VARCHAR(64) NOT NULL COMMENT "严重度",
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_vr_code` (`tenant_id`, `code`, `version_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="校验规则(14 维,规则 JSON 形态 script_type=1)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_validation_result` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图ID",
|
||||
`blueprint_version_no` INT NOT NULL DEFAULT 0 COMMENT "校验时版本",
|
||||
`dimension_count` INT NOT NULL DEFAULT 0 COMMENT "维度数(=14)",
|
||||
`passed_count` INT NOT NULL DEFAULT 0 COMMENT "通过数",
|
||||
`failed_count` INT NOT NULL DEFAULT 0 COMMENT "失败数",
|
||||
`score` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT "得分",
|
||||
`quality_state` VARCHAR(64) NOT NULL COMMENT "质量状态",
|
||||
`findings_json` LONGTEXT NULL COMMENT "逐维结论",
|
||||
`rule_set_version` VARCHAR(64) NOT NULL COMMENT "规则集版本",
|
||||
`created_by` VARCHAR(32) NOT NULL COMMENT "触发人",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_vres_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="校验结果(14 维 + 5 级质量状态)";
|
||||
|
||||
4
wwwroot/api/pbl_validation_get.dspy
Normal file
4
wwwroot/api/pbl_validation_get.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# pbl_validation/api/pbl_validation_get.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('pbl_validation/api/pbl_validation_get.dspy: START params_kw={dict(params_kw)}')
|
||||
data = await pbl_validation_get(**params_kw)
|
||||
return data
|
||||
4
wwwroot/api/pbl_validation_list.dspy
Normal file
4
wwwroot/api/pbl_validation_list.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# pbl_validation/api/pbl_validation_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('pbl_validation/api/pbl_validation_list.dspy: START params_kw={dict(params_kw)}')
|
||||
data = await pbl_validation_list(**params_kw)
|
||||
return data
|
||||
4
wwwroot/api/pbl_validation_rule_list.dspy
Normal file
4
wwwroot/api/pbl_validation_rule_list.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# pbl_validation/api/pbl_validation_rule_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('pbl_validation/api/pbl_validation_rule_list.dspy: START params_kw={dict(params_kw)}')
|
||||
data = await pbl_validation_rule_list(**params_kw)
|
||||
return data
|
||||
4
wwwroot/api/pbl_validation_rule_save.dspy
Normal file
4
wwwroot/api/pbl_validation_rule_save.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# pbl_validation/api/pbl_validation_rule_save.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('pbl_validation/api/pbl_validation_rule_save.dspy: START params_kw={dict(params_kw)}')
|
||||
data = await pbl_validation_rule_save(**params_kw)
|
||||
return data
|
||||
4
wwwroot/api/pbl_validation_run.dspy
Normal file
4
wwwroot/api/pbl_validation_run.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# pbl_validation/api/pbl_validation_run.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('pbl_validation/api/pbl_validation_run.dspy: START params_kw={dict(params_kw)}')
|
||||
data = await pbl_validation_run(**params_kw)
|
||||
return data
|
||||
175
wwwroot/index.ui
Normal file
175
wwwroot/index.ui
Normal file
@ -0,0 +1,175 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "20px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "PBL 校验引擎(14 维 + 5 级质量状态,M2)",
|
||||
"fontSize": "24px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "ResponsableBox",
|
||||
"options": {
|
||||
"gap": "16px",
|
||||
"minWidth": "250px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.pbl_validation_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/pbl_validation_run.dspy')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "pbl_validation_run"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.pbl_validation_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/pbl_validation_get.dspy')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "pbl_validation_get"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.pbl_validation_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/pbl_validation_list.dspy')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "pbl_validation_list"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.pbl_validation_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/pbl_validation_rule_list.dspy')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "pbl_validation_rule_list"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.pbl_validation_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/pbl_validation_rule_save.dspy')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "pbl_validation_rule_save"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "pbl_validation_content",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"flex": "1",
|
||||
"marginTop": "20px"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user