feat(pbl_appcodes): 初始提交 - pbls 项目模块代码入库
This commit is contained in:
parent
73403b155f
commit
c08ab6a278
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/
|
||||||
53
init/data.json
Normal file
53
init/data.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"appcodes": {
|
||||||
|
"pbl_quality_state": [
|
||||||
|
"draft",
|
||||||
|
"needs_work",
|
||||||
|
"pbl_ready",
|
||||||
|
"playtest_ready",
|
||||||
|
"publish_ready"
|
||||||
|
],
|
||||||
|
"pbl_approval_status": [
|
||||||
|
"none",
|
||||||
|
"pending",
|
||||||
|
"approved",
|
||||||
|
"rejected",
|
||||||
|
"withdrawn"
|
||||||
|
],
|
||||||
|
"pbl_visibility": [
|
||||||
|
"private",
|
||||||
|
"class",
|
||||||
|
"school",
|
||||||
|
"public_readonly"
|
||||||
|
],
|
||||||
|
"pbl_evidence_type": [
|
||||||
|
"artifact",
|
||||||
|
"process_event",
|
||||||
|
"reflection",
|
||||||
|
"feedback"
|
||||||
|
],
|
||||||
|
"pbl_agent_type": [
|
||||||
|
"designer",
|
||||||
|
"critic"
|
||||||
|
],
|
||||||
|
"pbl_tool_state": [
|
||||||
|
"enabled",
|
||||||
|
"disabled_contract_only"
|
||||||
|
],
|
||||||
|
"pbl_artifact_type": [
|
||||||
|
"document",
|
||||||
|
"presentation",
|
||||||
|
"model",
|
||||||
|
"code",
|
||||||
|
"media"
|
||||||
|
],
|
||||||
|
"pbl_event_type": [
|
||||||
|
"mission.completed",
|
||||||
|
"task.completed",
|
||||||
|
"pbl.unlockMission",
|
||||||
|
"entity.state.changed",
|
||||||
|
"session.member.joined",
|
||||||
|
"artifact.submitted"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
19
pbl_appcodes/__init__.py
Normal file
19
pbl_appcodes/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""pbl_appcodes —— PBL 枚举编码注入(6+2 组,幂等,零表)
|
||||||
|
|
||||||
|
注册三处同步之 ②:必须导出 init.py 里的全部契约函数,漏一行 .dspy 调用即 NameError。
|
||||||
|
"""
|
||||||
|
from pbl_appcodes.init import load_pbl_appcodes
|
||||||
|
from pbl_appcodes.api import (
|
||||||
|
pbl_appcodes_inject,
|
||||||
|
pbl_appcodes_list,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'load_pbl_appcodes',
|
||||||
|
'pbl_appcodes_inject',
|
||||||
|
'pbl_appcodes_list',
|
||||||
|
|
||||||
|
]
|
||||||
70
pbl_appcodes/api.py
Normal file
70
pbl_appcodes/api.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""pbl_appcodes.api —— 枚举 6+2 组幂等注入(DDL 零 ENUM、零 INSERT,编码集中于此)。
|
||||||
|
|
||||||
|
事实源:modules/pbl_appcodes/init/data.json(由 apps/pbls/scripts/gen_artifacts.py 同源生成)。
|
||||||
|
幂等:按 (tenant_id, appcode, codename) 已存在即跳过;重复执行不产生重复行。
|
||||||
|
G6 范围纪律:本模块只写 appcodes 表,不写 pbl_kdb_item / research 任何表。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pbl_common.api import PblError, dbname, now_str, sql_exec, sql_rows, tenant_id
|
||||||
|
|
||||||
|
SEED_RELPATH = os.path.join('init', 'data.json')
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_path():
|
||||||
|
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # modules/pbl_appcodes
|
||||||
|
return os.path.join(here, SEED_RELPATH)
|
||||||
|
|
||||||
|
|
||||||
|
def load_seed():
|
||||||
|
p = _seed_path()
|
||||||
|
if not os.path.exists(p):
|
||||||
|
raise PblError('PBL_APPCODES_SEED_MISSING', '种子文件缺失:%s' % p)
|
||||||
|
with open(p, encoding='utf-8') as f:
|
||||||
|
return json.load(f)['appcodes']
|
||||||
|
|
||||||
|
|
||||||
|
async def pbl_appcodes_inject(**kw):
|
||||||
|
"""幂等注入 6+2 组枚举。返回 {groups, inserted, skipped}。"""
|
||||||
|
groups = load_seed()
|
||||||
|
only = kw.get('appcode')
|
||||||
|
if only:
|
||||||
|
groups = {only: groups.get(only) or []}
|
||||||
|
if not groups[only]:
|
||||||
|
raise PblError('PBL_APPCODE_UNKNOWN', '未知枚举组 %s' % only)
|
||||||
|
tid = await tenant_id(required=False)
|
||||||
|
inserted = skipped = 0
|
||||||
|
for code, names in groups.items():
|
||||||
|
for i, name in enumerate(names):
|
||||||
|
exist = await sql_rows(
|
||||||
|
'SELECT `id` FROM `appcodes` WHERE `appcode` = ${a}$ AND `codename` = ${c}$'
|
||||||
|
' AND `tenant_id` = ${t}$ LIMIT 1',
|
||||||
|
{'a': code, 'c': name, 't': tid}, 'pbl')
|
||||||
|
if exist:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
await sql_exec(
|
||||||
|
'INSERT INTO `appcodes` (`tenant_id`,`appcode`,`codename`,`codename_cn`,'
|
||||||
|
'`order_no`,`is_enabled`,`created_at`) '
|
||||||
|
'VALUES (${t}$,${a}$,${c}$,${n}$,${o}$,1,${ts}$)',
|
||||||
|
{'t': tid, 'a': code, 'c': name, 'n': name, 'o': (i + 1) * 10,
|
||||||
|
'ts': now_str()}, 'pbl')
|
||||||
|
inserted += 1
|
||||||
|
return {'ok': True, 'groups': len(groups), 'inserted': inserted, 'skipped': skipped,
|
||||||
|
'keys': sorted(groups)}
|
||||||
|
|
||||||
|
|
||||||
|
async def pbl_appcodes_list(**kw):
|
||||||
|
"""读取枚举组(供前端下拉/校验白名单,只读)。"""
|
||||||
|
tid = await tenant_id(required=False)
|
||||||
|
sql = ('SELECT `appcode`,`codename`,`order_no` FROM `appcodes` WHERE `tenant_id` = ${t}$'
|
||||||
|
+ (' AND `appcode` = ${a}$' if kw.get('appcode') else '')
|
||||||
|
+ ' ORDER BY `appcode`,`order_no`')
|
||||||
|
rows = await sql_rows(sql, {'t': tid, 'a': kw.get('appcode')}, 'pbl')
|
||||||
|
out = {}
|
||||||
|
for r in rows:
|
||||||
|
out.setdefault(r['appcode'], []).append(r['codename'])
|
||||||
|
return {'ok': True, 'groups': out, 'counts': {k: len(v) for k, v in out.items()}}
|
||||||
21
pbl_appcodes/init.py
Normal file
21
pbl_appcodes/init.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""`load_pbl_appcodes()` —— pbl_appcodes 模块唯一挂载入口。
|
||||||
|
|
||||||
|
注册三处同步之 ③:env.<契约名> = <契约名>(① 定义在 api.py,② 导出在 __init__.py)。
|
||||||
|
"""
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
|
||||||
|
from pbl_appcodes.api import (
|
||||||
|
pbl_appcodes_inject,
|
||||||
|
pbl_appcodes_list,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_pbl_appcodes():
|
||||||
|
env = ServerEnv()
|
||||||
|
env.pbl_appcodes_inject = pbl_appcodes_inject
|
||||||
|
env.pbl_appcodes_list = pbl_appcodes_list
|
||||||
|
|
||||||
|
return 'pbl_appcodes'
|
||||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "pbl_appcodes"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "PBL 枚举编码注入(6+2 组,幂等,零表)"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ["pbl_appcodes"]
|
||||||
41
scripts/load_path.py
Normal file
41
scripts/load_path.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""pbl_appcodes RBAC 路径注册(硬门禁 6.6 / QC #11)。
|
||||||
|
|
||||||
|
约定:
|
||||||
|
- 路径 = 模块自动路由 `/pbl_appcodes/api/<契约>.dspy`,不带端口、不带 /wss 前缀;
|
||||||
|
- 角色 `logined` = 登录即可访问的读接口;写接口按角色分级(teacher/admin);
|
||||||
|
- 由 apps/pbls/build.sh 第 8 步调用 `register()`;rbac CLI 不在位时打印清单(不静默跳过)。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
MODULE = 'pbl_appcodes'
|
||||||
|
|
||||||
|
# (path, role)
|
||||||
|
PATHS = [
|
||||||
|
('/pbl_appcodes/api/pbl_appcodes_inject.dspy', 'logined'),
|
||||||
|
('/pbl_appcodes/api/pbl_appcodes_list.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)
|
||||||
20
skill/SKILL.md
Normal file
20
skill/SKILL.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# pbl_appcodes 模块技能(自动生成骨架 + 人工补充)
|
||||||
|
|
||||||
|
## 定位
|
||||||
|
PBL 枚举编码注入(6+2 组,幂等,零表)
|
||||||
|
|
||||||
|
## 挂载
|
||||||
|
`from pbl_appcodes.init import load_pbl_appcodes` → `load_pbl_appcodes()`(应用 app/pbls.py init() 中按序调用)
|
||||||
|
|
||||||
|
## 数据表(0 张)
|
||||||
|
- 无(零表模块)
|
||||||
|
|
||||||
|
## 契约接口(2 个,路径 `/pbl_appcodes/api/<name>.dspy`)
|
||||||
|
- `pbl_appcodes_inject`
|
||||||
|
- `pbl_appcodes_list`
|
||||||
|
|
||||||
|
## 陷阱
|
||||||
|
- 库名一律 `ServerEnv().get_module_dbname('pbl_appcodes')`,禁止硬编码 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 路径。
|
||||||
4
wwwroot/api/pbl_appcodes_inject.dspy
Normal file
4
wwwroot/api/pbl_appcodes_inject.dspy
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# pbl_appcodes/api/pbl_appcodes_inject.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||||
|
debug('pbl_appcodes/api/pbl_appcodes_inject.dspy: START params_kw={dict(params_kw)}')
|
||||||
|
data = await pbl_appcodes_inject(**params_kw)
|
||||||
|
return data
|
||||||
4
wwwroot/api/pbl_appcodes_list.dspy
Normal file
4
wwwroot/api/pbl_appcodes_list.dspy
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# pbl_appcodes/api/pbl_appcodes_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||||
|
debug('pbl_appcodes/api/pbl_appcodes_list.dspy: START params_kw={dict(params_kw)}')
|
||||||
|
data = await pbl_appcodes_list(**params_kw)
|
||||||
|
return data
|
||||||
91
wwwroot/index.ui
Normal file
91
wwwroot/index.ui
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"height": "100%",
|
||||||
|
"padding": "20px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "PBL 枚举编码注入(6+2 组,幂等,零表)",
|
||||||
|
"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_appcodes_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('api/pbl_appcodes_inject.dspy')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "pbl_appcodes_inject"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"padding": "20px",
|
||||||
|
"cursor": "pointer"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.pbl_appcodes_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('api/pbl_appcodes_list.dspy')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "pbl_appcodes_list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "pbl_appcodes_content",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"flex": "1",
|
||||||
|
"marginTop": "20px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user