labull-framework 1.4.1__tar.gz → 2.0.0__tar.gz

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.
Files changed (68) hide show
  1. {labull_framework-1.4.1/labull_framework.egg-info → labull_framework-2.0.0}/PKG-INFO +3 -2
  2. {labull_framework-1.4.1 → labull_framework-2.0.0}/README-pypi.md +206 -205
  3. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/admin.py +24 -4
  4. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/apps.py +5 -1
  5. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/auth/backend.py +46 -4
  6. labull_framework-2.0.0/labull_framework/auth/bootstrap.py +98 -0
  7. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/auth/views.py +5 -2
  8. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/env.py +1 -1
  9. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/flow.py +147 -123
  10. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/devkit.py +8 -7
  11. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/dispatch.py +4 -2
  12. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/health.py +14 -5
  13. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/sdk.py +3 -2
  14. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/types/workflow.py +15 -20
  15. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/views.py +19 -16
  16. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/management/commands/labull_blocktype.py +19 -11
  17. labull_framework-2.0.0/labull_framework/migrations/0005_labullrole.py +62 -0
  18. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/models.py +79 -1
  19. labull_framework-2.0.0/labull_framework/roles.py +189 -0
  20. {labull_framework-1.4.1 → labull_framework-2.0.0/labull_framework.egg-info}/PKG-INFO +3 -2
  21. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework.egg-info/SOURCES.txt +2 -0
  22. {labull_framework-1.4.1 → labull_framework-2.0.0}/pyproject.toml +1 -1
  23. labull_framework-1.4.1/labull_framework/auth/bootstrap.py +0 -75
  24. {labull_framework-1.4.1 → labull_framework-2.0.0}/LICENSE +0 -0
  25. {labull_framework-1.4.1 → labull_framework-2.0.0}/MANIFEST.in +0 -0
  26. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/__init__.py +0 -0
  27. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/auth/__init__.py +0 -0
  28. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/auth/sso.py +0 -0
  29. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/canvas/__init__.py +0 -0
  30. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/canvas/data.py +0 -0
  31. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/canvas/gitinfo.py +0 -0
  32. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/canvas/shape_ast.py +0 -0
  33. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/canvas/static/canvas.css +0 -0
  34. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/canvas/static/canvas.js +0 -0
  35. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/canvas/static/page.html +0 -0
  36. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/canvas/views.py +0 -0
  37. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/jsonio.py +0 -0
  38. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/__init__.py +0 -0
  39. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/blockcheck.py +0 -0
  40. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/blockpackage.py +0 -0
  41. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/blocktypes.py +0 -0
  42. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/deepcheck.py +0 -0
  43. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/export.py +0 -0
  44. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/manifest.py +0 -0
  45. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/parallel.py +0 -0
  46. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/registry.py +0 -0
  47. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/requires.py +0 -0
  48. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/service.py +0 -0
  49. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/subproc.py +0 -0
  50. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/types/__init__.py +0 -0
  51. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/types/plain.py +0 -0
  52. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/magic_block/validate.py +0 -0
  53. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/management/__init__.py +0 -0
  54. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/management/commands/__init__.py +0 -0
  55. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/management/commands/labull_contract.py +0 -0
  56. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/management/commands/labull_requires.py +0 -0
  57. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/migrations/0001_initial.py +0 -0
  58. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/migrations/0002_remove_magicblock_prev_files_and_more.py +0 -0
  59. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/migrations/0003_magicblock_tags.py +0 -0
  60. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/migrations/0004_magicblock_detail.py +0 -0
  61. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/migrations/__init__.py +0 -0
  62. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/settings.py +0 -0
  63. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/urls.py +0 -0
  64. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework/virtual.py +0 -0
  65. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework.egg-info/dependency_links.txt +0 -0
  66. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework.egg-info/requires.txt +0 -0
  67. {labull_framework-1.4.1 → labull_framework-2.0.0}/labull_framework.egg-info/top_level.txt +0 -0
  68. {labull_framework-1.4.1 → labull_framework-2.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 1.4.1
3
+ Version: 2.0.0
4
4
  Summary: Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks
5
5
  Author: Labull Framework
6
6
  License-Expression: MIT
@@ -93,7 +93,8 @@ CORS_ORIGIN=http://localhost:5173
93
93
 
94
94
  | 想要什么 | 怎么做 |
95
95
  |---|---|
96
- | **谁是管理员** | 配 `BOOTSTRAP_ADMIN_WORK_IDS=工号,工号` —— 它是**声明式**的名单:启动时名单里的人补上 `is_staff`,**名单之外的一律清掉**(没配就是"一个管理员都没有")。它不碰 `is_superuser`,所以别拿超管当后门 |
96
+ | **谁是管理员** | 配 `BOOTSTRAP_ADMIN_WORK_IDS=工号,工号` —— 它是**声明式**的名单:启动时名单里的人加进**管理员角色**的组,**名单之外的一律摘掉**(没配就是"一个管理员都没有")。它不碰 `is_superuser`,所以别拿超管当后门 |
97
+ | **管理员 / 默认角色** | **默认就有**:SDK 的 `0005` 迁移种下 `ADMIN`(管理员角色)/ `USER`(默认角色,新用户第一次登录自动加入)。要换码或组名就写项目自己的 data migration;两条**各恰好一条、不能同一行**,缺了 / 重了服务起不来 |
97
98
  | **看数据模型那一页** | 设 `LABULL_SCHEMA_CANVAS=on`,浏览器打开 `/` |
98
99
  | **块的依赖声明不许"用了却没声明"** | 它是**默认**行为(`LABULL_REQUIRES=error`,1.0.1 起):代码里用了没声明的东西 ⇒ 拦住保存。⚠️ 升级前先在项目里跑一遍 `python manage.py labull_requires --write` 把存量块扫得到的声明补进库;`warn`(只提示不拦)与 `off`(整段不跑)都要**显式**写 |
99
100
  | **导出一份模型形状** | `python manage.py labull_contract --out .labull/contract.json` |
@@ -1,205 +1,206 @@
1
- # labull-framework
2
-
3
- 一个可插拔的 **Django app**(import 名 `labull_framework`),提供两件事:
4
-
5
- - **Authing / OIDC 登录**:自带的认证后端,只用标准库 + PyJWT,不依赖任何 OIDC 客户端包。
6
- - **magic block**:一个 uuid + 一整个文件包就是一个功能块,住在数据库里、在沙箱 iframe 里渲染,
7
- 不重新编译、不重启;块可以带自己的后端 Python(`backend_open.py`)。
8
-
9
- 另附一个可选的 **数据模型页**(`LABULL_SCHEMA_CANVAS=on` 才开)。
10
-
11
- ## 安装
12
-
13
- ```bash
14
- pip install labull-framework
15
- ```
16
-
17
- ## 接进一个 Django 项目
18
-
19
- `settings.py` 里三件事:
20
-
21
- ```python
22
- from labull_framework.env import load_env_file
23
- load_env_file(BASE_DIR / '.env') # ① 第一行:读 .env
24
-
25
- AUTH_USER_MODEL = 'labull_framework.User' # ② 第一次 migrate 之前就要设好
26
-
27
- from labull_framework.settings import apply_platform_settings
28
- apply_platform_settings(globals()) # ③ 最后一行:把平台的设置补进来
29
- ```
30
-
31
- `urls.py` 里一行:
32
-
33
- ```python
34
- from labull_framework.urls import urlpatterns as labull_urls
35
- urlpatterns = [*labull_urls, path('admin/', admin.site.urls)]
36
- ```
37
-
38
- `wsgi.py` 里一行(**别漏**,重设管理员名单与启动体检都在这里):
39
-
40
- ```python
41
- from labull_framework.magic_block.health import startup_once
42
- startup_once()
43
- ```
44
-
45
- 然后:
46
-
47
- ```bash
48
- python manage.py migrate
49
- python manage.py runserver
50
- ```
51
-
52
- `.env` 最少要这几项:
53
-
54
- ```ini
55
- DATABASE_URL=postgres://postgres:postgres@localhost:5432/app
56
- SSO_ISSUER=https://<你的域名>.authing.cn/oidc
57
- SSO_CLIENT_ID=<Authing 应用 ID>
58
- SSO_CLIENT_SECRET=<Authing 应用密钥>
59
- SSO_REDIRECT_URI=http://localhost:8000/auth/callback
60
- SSO_WEB_ORIGIN=http://localhost:5173
61
- CORS_ORIGIN=http://localhost:5173
62
- ```
63
-
64
- `sqlite:///db.sqlite3` 也认,本地开发不必装数据库。
65
-
66
- | 想要什么 | 怎么做 |
67
- |---|---|
68
- | **谁是管理员** | 配 `BOOTSTRAP_ADMIN_WORK_IDS=工号,工号` —— 它是**声明式**的名单:启动时名单里的人补上 `is_staff`,**名单之外的一律清掉**(没配就是"一个管理员都没有")。它不碰 `is_superuser`,所以别拿超管当后门 |
69
- | **看数据模型那一页** | 设 `LABULL_SCHEMA_CANVAS=on`,浏览器打开 `/` |
70
- | **块的依赖声明不许"用了却没声明"** | 它是**默认**行为(`LABULL_REQUIRES=error`,1.0.1 起):代码里用了没声明的东西 ⇒ 拦住保存。⚠️ 升级前先在项目里跑一遍 `python manage.py labull_requires --write` 把存量块扫得到的声明补进库;`warn`(只提示不拦)与 `off`(整段不跑)都要**显式**写 |
71
- | **导出一份模型形状** | `python manage.py labull_contract --out .labull/contract.json` |
72
-
73
- ## magic block
74
-
75
- 一个块 = **一个 uuid + 一整个文件包**:
76
-
77
- ```
78
- 我的块/
79
- ├─ index.open.html 入口(必备)
80
- ├─ magic-block.open.yaml 清单(必备):`id` / `module` / `name` / `brief` 四个键必填
81
- │ + `requires`(依赖声明:用了哪些模型 / pip 包 / settings 键 / `BLOCK_*`)可选
82
- ├─ README.open.md **业务功能说明**(Markdown):这一段的家就是这份文件,不在清单里
83
- │ (**每个块都会有一份** —— 导出时无条件铺,没写就是空文件)
84
- ├─ requires.open.yaml 依赖声明的**草稿**(导出铺下来的:正文进清单的 `requires`,注释是扫代码的建议)
85
- ├─ 任意 js / css
86
- ├─ .exclude/ 你自己的地盘(测试 / 草稿):**永远不进库**,也不跟着块走
87
- └─ backend_open.py 块自己的后端(可选)
88
- ```
89
-
90
- 测试 / 草稿 / 参考实现放块里的 `.exclude/`(老名字 `excluded/` 在迁移期照样认):判据只有一条 ——
91
- **路径里有一段叫 `.exclude`**,与文件名怎么写无关(`test.py` / `_test.py` 一视同仁)。里面的东西
92
- **永远不进库**:导出不铺、读回不扫、保存不存、版本史里没有;「清空并导出」也不会删它,它是**你这台机器上的**
93
- 草稿区。摆法是**一个需求一个文件夹**(`<日期>-<需求简称>`,最多留 3 个),测试代码与测试产物都放进去。
94
- 平台不会替你跑它们,自己 `node` / `python` 跑:`node .exclude/<需求文件夹>/test.mjs`。
95
-
96
- 块后端与项目里别的 Python 代码没有区别:
97
-
98
- ```python
99
- from labull_framework.magic_block.sdk import MagicBackendAPI, errors
100
-
101
- class Order:
102
- @MagicBackendAPI() # 省略 permission = 任何登录用户都能调
103
- def list(self, ctx, params):
104
- from modules.orders.models import Order
105
- return [{'id': o.id} for o in Order.objects.all()[:20]]
106
-
107
- @MagicBackendAPI(permission='labull_framework.edit_blocks')
108
- def close(self, ctx, params):
109
- if not params.get('id'):
110
- raise errors.BadRequest('要给我一个 id')
111
- return {'ok': True}
112
- ```
113
-
114
- 配置:环境变量里**带 `BLOCK_` 前缀**的才会进 `ctx.config`(`BLOCK_OCR_KEY=a` ⇒ `ctx.config['OCR_KEY']`)。
115
-
116
- 🔴 **弹窗走平台**:块里 `await magicBlock.dialog({ title, body, fields, buttons })` —— 由宿主页面渲染
117
- (前端要挂一次 `<MagicBlockDialogs />`)。块自己画的浮层**摆不正**:块可以是好几屏高,块里
118
- `position: fixed` / `100vh` 的基准是"整块高度"而不是用户那一屏。要自己写整篇就传 `{ html }`,
119
- 宿主给一个确定大小的面板 + 另一格沙箱 iframe 装它,里面用 `magicBlock.done({ button, values })` 交回选择。
120
-
121
- ## 虚拟类(`@virtual_class`)
122
-
123
- 几张表共有同一批列时,写一个**虚拟类**:一个**不建表的抽象模型**,自己那几列会被继承到子表上。
124
-
125
- ```python
126
- from django.db import models
127
- from labull_framework.virtual import virtual_class, VirtualModel
128
-
129
-
130
- @virtual_class
131
- class Contact(VirtualModel):
132
- """联系人:个人与单位共有的那几列。"""
133
- name = models.CharField('名称', max_length=64)
134
-
135
- class Meta:
136
- abstract = True # 🔴 必须有(见下)
137
-
138
-
139
- class Person(Contact):
140
- birthday = models.DateField('生日', null=True, blank=True)
141
-
142
-
143
- class Company(Contact):
144
- tax_no = models.CharField('税号', max_length=32, blank=True, default='')
145
- ```
146
-
147
- `Person` / `Company` 的表里**真的有** `name` 这一列(Django 把抽象基类的字段复制进子类);
148
- 而 `makemigrations` **只为这两张具体表建表** —— `Contact` 在库里没有对应的表。
149
-
150
- 两条会真的出错的判据:
151
-
152
- 1. **`class Meta: abstract = True` 不能省**(`@virtual_class` 替不了它:Django 在 `ModelBase` 里
153
- 读的是**类体那个 `Meta`**,读完就把非抽象模型登记进 app registry 了)。漏了会让这个类**真的建一张表**,
154
- 所以装饰器发现这件事时**当场抛 `TypeError`** 并把该补的那一行写给你。
155
- 2. **`@virtual_class` 写在所有装饰器的最上面**(它执行时那个类必须已经是一个 Django 模型);
156
- 另外**外键不能指向虚拟类**(抽象模型没有 `objects`,Django 在 `manage.py check` 就拦下)。
157
-
158
- `LABULL_SCHEMA_CANVAS=on` 时打开 `/`:虚拟类是一张带 `◇` 的虚线卡片(写着"不建表"与"谁在继承它"),
159
- 继承它的表表头挂着「**◇ 泛化 · Contact**」—— 鼠标停一下看泛化名与整条链,点一下跳过去。
160
- 改动(含"哪张表的泛化父类变了")与表一样进画布那份差异。
161
-
162
- ### 平台自带:WorkflowModel(流转记录)
163
-
164
- 要记"这件事是谁、什么时候、怎么走过来的"的表,**继承它就行**(不用自己写抽象基类):
165
-
166
- ```python
167
- from django.db import models
168
- from labull_framework.models import WorkflowModel
169
-
170
-
171
- class PurchaseOrder(WorkflowModel):
172
- """采购申请:全过程记在继承来的 log 那一列里。"""
173
- title = models.CharField('标题', max_length=128)
174
-
175
- class Meta:
176
- verbose_name = '采购申请' # 🔴 一定要自己写(不然继承来的是基类那个名字)
177
- ```
178
-
179
- 它给子表带上三列:`log`(流转日志,JSON 数组)、`created_at`(创建时间)与 `updated_at`(自动时间)。
180
- 读写走类上的助手,别在调用点自己拼字典:
181
-
182
- ```python
183
- WorkflowModel.append_log(row, 'submit', actor=user.name, note='提交审批')
184
- row.save(update_fields=['log'])
185
- WorkflowModel.log_of(row) # → [{'at': ..., 'action': 'submit', ...}, ...]
186
- WorkflowModel.last_log(row, 'approve') # → 最后一次批准(没有就 None)
187
- ```
188
-
189
- 画布上它是一张带 `◇` 的**虚拟卡片**(在 `labull_framework` 模块里),
190
- 继承它的表表头挂着「◇ 泛化 · WorkflowModel」。
191
-
192
- - **当前状态用列**(要能筛选、能索引),`log` 只回答"怎么走到这一步的"。
193
- - `log` 是**一列 JSON、不是一张子表**(刻意取舍):代价是跨表查流水要全表扫 JSON ——
194
- 真到那一步就该给它建一张真表。
195
- - 🔴 **块后端不要 import 它**(平台内部模型),开发包的清单里也不会给出那句 import 提示。
196
-
197
- ## 运行环境
198
-
199
- - Python ≥ 3.10
200
- - Django ≥ 4.2(在 6.1 上验证过)
201
- - PyJWT(带 `crypto` 后端)
202
-
203
- ## License
204
-
205
- MIT
1
+ # labull-framework
2
+
3
+ 一个可插拔的 **Django app**(import 名 `labull_framework`),提供两件事:
4
+
5
+ - **Authing / OIDC 登录**:自带的认证后端,只用标准库 + PyJWT,不依赖任何 OIDC 客户端包。
6
+ - **magic block**:一个 uuid + 一整个文件包就是一个功能块,住在数据库里、在沙箱 iframe 里渲染,
7
+ 不重新编译、不重启;块可以带自己的后端 Python(`backend_open.py`)。
8
+
9
+ 另附一个可选的 **数据模型页**(`LABULL_SCHEMA_CANVAS=on` 才开)。
10
+
11
+ ## 安装
12
+
13
+ ```bash
14
+ pip install labull-framework
15
+ ```
16
+
17
+ ## 接进一个 Django 项目
18
+
19
+ `settings.py` 里三件事:
20
+
21
+ ```python
22
+ from labull_framework.env import load_env_file
23
+ load_env_file(BASE_DIR / '.env') # ① 第一行:读 .env
24
+
25
+ AUTH_USER_MODEL = 'labull_framework.User' # ② 第一次 migrate 之前就要设好
26
+
27
+ from labull_framework.settings import apply_platform_settings
28
+ apply_platform_settings(globals()) # ③ 最后一行:把平台的设置补进来
29
+ ```
30
+
31
+ `urls.py` 里一行:
32
+
33
+ ```python
34
+ from labull_framework.urls import urlpatterns as labull_urls
35
+ urlpatterns = [*labull_urls, path('admin/', admin.site.urls)]
36
+ ```
37
+
38
+ `wsgi.py` 里一行(**别漏**,重设管理员名单与启动体检都在这里):
39
+
40
+ ```python
41
+ from labull_framework.magic_block.health import startup_once
42
+ startup_once()
43
+ ```
44
+
45
+ 然后:
46
+
47
+ ```bash
48
+ python manage.py migrate
49
+ python manage.py runserver
50
+ ```
51
+
52
+ `.env` 最少要这几项:
53
+
54
+ ```ini
55
+ DATABASE_URL=postgres://postgres:postgres@localhost:5432/app
56
+ SSO_ISSUER=https://<你的域名>.authing.cn/oidc
57
+ SSO_CLIENT_ID=<Authing 应用 ID>
58
+ SSO_CLIENT_SECRET=<Authing 应用密钥>
59
+ SSO_REDIRECT_URI=http://localhost:8000/auth/callback
60
+ SSO_WEB_ORIGIN=http://localhost:5173
61
+ CORS_ORIGIN=http://localhost:5173
62
+ ```
63
+
64
+ `sqlite:///db.sqlite3` 也认,本地开发不必装数据库。
65
+
66
+ | 想要什么 | 怎么做 |
67
+ |---|---|
68
+ | **谁是管理员** | 配 `BOOTSTRAP_ADMIN_WORK_IDS=工号,工号` —— 它是**声明式**的名单:启动时名单里的人加进**管理员角色**的组,**名单之外的一律摘掉**(没配就是"一个管理员都没有")。它不碰 `is_superuser`,所以别拿超管当后门 |
69
+ | **管理员 / 默认角色** | **默认就有**:SDK 的 `0005` 迁移种下 `ADMIN`(管理员角色)/ `USER`(默认角色,新用户第一次登录自动加入)。要换码或组名就写项目自己的 data migration;两条**各恰好一条、不能同一行**,缺了 / 重了服务起不来 |
70
+ | **看数据模型那一页** | 设 `LABULL_SCHEMA_CANVAS=on`,浏览器打开 `/` |
71
+ | **块的依赖声明不许"用了却没声明"** | 它是**默认**行为(`LABULL_REQUIRES=error`,1.0.1 起):代码里用了没声明的东西 ⇒ 拦住保存。⚠️ 升级前先在项目里跑一遍 `python manage.py labull_requires --write` 把存量块扫得到的声明补进库;`warn`(只提示不拦)与 `off`(整段不跑)都要**显式**写 |
72
+ | **导出一份模型形状** | `python manage.py labull_contract --out .labull/contract.json` |
73
+
74
+ ## magic block
75
+
76
+ 一个块 = **一个 uuid + 一整个文件包**:
77
+
78
+ ```
79
+ 我的块/
80
+ ├─ index.open.html 入口(必备)
81
+ ├─ magic-block.open.yaml 清单(必备):`id` / `module` / `name` / `brief` 四个键必填
82
+ │ + `requires`(依赖声明:用了哪些模型 / pip 包 / settings 键 / `BLOCK_*`)可选
83
+ ├─ README.open.md **业务功能说明**(Markdown):这一段的家就是这份文件,不在清单里
84
+ │ (**每个块都会有一份** —— 导出时无条件铺,没写就是空文件)
85
+ ├─ requires.open.yaml 依赖声明的**草稿**(导出铺下来的:正文进清单的 `requires`,注释是扫代码的建议)
86
+ ├─ 任意 js / css
87
+ ├─ .exclude/ 你自己的地盘(测试 / 草稿):**永远不进库**,也不跟着块走
88
+ └─ backend_open.py 块自己的后端(可选)
89
+ ```
90
+
91
+ 测试 / 草稿 / 参考实现放块里的 `.exclude/`(老名字 `excluded/` 在迁移期照样认):判据只有一条 ——
92
+ **路径里有一段叫 `.exclude`**,与文件名怎么写无关(`test.py` / `_test.py` 一视同仁)。里面的东西
93
+ **永远不进库**:导出不铺、读回不扫、保存不存、版本史里没有;「清空并导出」也不会删它,它是**你这台机器上的**
94
+ 草稿区。摆法是**一个需求一个文件夹**(`<日期>-<需求简称>`,最多留 3 个),测试代码与测试产物都放进去。
95
+ 平台不会替你跑它们,自己 `node` / `python` 跑:`node .exclude/<需求文件夹>/test.mjs`。
96
+
97
+ 块后端与项目里别的 Python 代码没有区别:
98
+
99
+ ```python
100
+ from labull_framework.magic_block.sdk import MagicBackendAPI, errors
101
+
102
+ class Order:
103
+ @MagicBackendAPI() # 省略 permission = 任何登录用户都能调
104
+ def list(self, ctx, params):
105
+ from modules.orders.models import Order
106
+ return [{'id': o.id} for o in Order.objects.all()[:20]]
107
+
108
+ @MagicBackendAPI(permission='labull_framework.edit_blocks')
109
+ def close(self, ctx, params):
110
+ if not params.get('id'):
111
+ raise errors.BadRequest('要给我一个 id')
112
+ return {'ok': True}
113
+ ```
114
+
115
+ 配置:环境变量里**带 `BLOCK_` 前缀**的才会进 `ctx.config`(`BLOCK_OCR_KEY=a` ⇒ `ctx.config['OCR_KEY']`)。
116
+
117
+ 🔴 **弹窗走平台**:块里 `await magicBlock.dialog({ title, body, fields, buttons })` —— 由宿主页面渲染
118
+ (前端要挂一次 `<MagicBlockDialogs />`)。块自己画的浮层**摆不正**:块可以是好几屏高,块里
119
+ `position: fixed` / `100vh` 的基准是"整块高度"而不是用户那一屏。要自己写整篇就传 `{ html }`,
120
+ 宿主给一个确定大小的面板 + 另一格沙箱 iframe 装它,里面用 `magicBlock.done({ button, values })` 交回选择。
121
+
122
+ ## 虚拟类(`@virtual_class`)
123
+
124
+ 几张表共有同一批列时,写一个**虚拟类**:一个**不建表的抽象模型**,自己那几列会被继承到子表上。
125
+
126
+ ```python
127
+ from django.db import models
128
+ from labull_framework.virtual import virtual_class, VirtualModel
129
+
130
+
131
+ @virtual_class
132
+ class Contact(VirtualModel):
133
+ """联系人:个人与单位共有的那几列。"""
134
+ name = models.CharField('名称', max_length=64)
135
+
136
+ class Meta:
137
+ abstract = True # 🔴 必须有(见下)
138
+
139
+
140
+ class Person(Contact):
141
+ birthday = models.DateField('生日', null=True, blank=True)
142
+
143
+
144
+ class Company(Contact):
145
+ tax_no = models.CharField('税号', max_length=32, blank=True, default='')
146
+ ```
147
+
148
+ `Person` / `Company` 的表里**真的有** `name` 这一列(Django 把抽象基类的字段复制进子类);
149
+ 而 `makemigrations` **只为这两张具体表建表** —— `Contact` 在库里没有对应的表。
150
+
151
+ 两条会真的出错的判据:
152
+
153
+ 1. **`class Meta: abstract = True` 不能省**(`@virtual_class` 替不了它:Django 在 `ModelBase` 里
154
+ 读的是**类体那个 `Meta`**,读完就把非抽象模型登记进 app registry 了)。漏了会让这个类**真的建一张表**,
155
+ 所以装饰器发现这件事时**当场抛 `TypeError`** 并把该补的那一行写给你。
156
+ 2. **`@virtual_class` 写在所有装饰器的最上面**(它执行时那个类必须已经是一个 Django 模型);
157
+ 另外**外键不能指向虚拟类**(抽象模型没有 `objects`,Django 在 `manage.py check` 就拦下)。
158
+
159
+ `LABULL_SCHEMA_CANVAS=on` 时打开 `/`:虚拟类是一张带 `◇` 的虚线卡片(写着"不建表"与"谁在继承它"),
160
+ 继承它的表表头挂着「**◇ 泛化 · Contact**」—— 鼠标停一下看泛化名与整条链,点一下跳过去。
161
+ 改动(含"哪张表的泛化父类变了")与表一样进画布那份差异。
162
+
163
+ ### 平台自带:WorkflowModel(流转记录)
164
+
165
+ 要记"这件事是谁、什么时候、怎么走过来的"的表,**继承它就行**(不用自己写抽象基类):
166
+
167
+ ```python
168
+ from django.db import models
169
+ from labull_framework.models import WorkflowModel
170
+
171
+
172
+ class PurchaseOrder(WorkflowModel):
173
+ """采购申请:全过程记在继承来的 log 那一列里。"""
174
+ title = models.CharField('标题', max_length=128)
175
+
176
+ class Meta:
177
+ verbose_name = '采购申请' # 🔴 一定要自己写(不然继承来的是基类那个名字)
178
+ ```
179
+
180
+ 它给子表带上三列:`log`(流转日志,JSON 数组)、`created_at`(创建时间)与 `updated_at`(自动时间)。
181
+ 读写走类上的助手,别在调用点自己拼字典:
182
+
183
+ ```python
184
+ WorkflowModel.append_log(row, 'submit', actor=user.name, note='提交审批')
185
+ row.save(update_fields=['log'])
186
+ WorkflowModel.log_of(row) # → [{'at': ..., 'action': 'submit', ...}, ...]
187
+ WorkflowModel.last_log(row, 'approve') # → 最后一次批准(没有就 None)
188
+ ```
189
+
190
+ 画布上它是一张带 `◇` 的**虚拟卡片**(在 `labull_framework` 模块里),
191
+ 继承它的表表头挂着「◇ 泛化 · WorkflowModel」。
192
+
193
+ - **当前状态用列**(要能筛选、能索引),`log` 只回答"怎么走到这一步的"。
194
+ - `log` 是**一列 JSON、不是一张子表**(刻意取舍):代价是跨表查流水要全表扫 JSON ——
195
+ 真到那一步就该给它建一张真表。
196
+ - 🔴 **块后端不要 import 它**(平台内部模型),开发包的清单里也不会给出那句 import 提示。
197
+
198
+ ## 运行环境
199
+
200
+ - Python ≥ 3.10
201
+ - Django ≥ 4.2(在 6.1 上验证过)
202
+ - PyJWT(带 `crypto` 后端)
203
+
204
+ ## License
205
+
206
+ MIT
@@ -1,6 +1,8 @@
1
1
  """Django admin 的注册处 —— 这个库**唯一**的管理界面。
2
2
 
3
3
  🔴 它就是 Django 原生的那一套:用户 / 组 / 权限的日常维护白拿,我们不写任何自己的管理页面。
4
+ 🔴 另外登记一张**项目级**的表:`LabullRole`(码 → Django 组名)。角色是**项目数据**、表默认空,
5
+ 框架不内置任何码;组要先在「组」里建好,这里只做配对。
4
6
  🔴 **块的代码在 admin 里只读**:它只能从 magic block 控制台写进去,因为那里有"保存即校验"那一关
5
7
  (`ast` + 子进程试加载)。admin 里放开编辑表单,就等于从 Django 的默认表单往库里塞
6
8
  **未经校验、之后会被 `exec` 执行**的代码,把"体检不过就停用"这套机制绕开。
@@ -11,7 +13,7 @@ from __future__ import annotations
11
13
 
12
14
  from django.contrib import admin
13
15
 
14
- from .models import MagicBlock, User
16
+ from .models import LabullRole, MagicBlock, User
15
17
 
16
18
 
17
19
  @admin.register(User)
@@ -20,21 +22,39 @@ class UserAdmin(admin.ModelAdmin):
20
22
 
21
23
  ⚠️ `username` 装的是 Authing 的 `sub`(登录时同步进来的),所以它**只读** ——
22
24
  改它等于把这一行与那个身份切开。
25
+ 🔴 **平台管理员是"管理员角色的组员"**(`LabullRole.is_admin` 那一行),这里不摆 `is_staff` ——
26
+ 那一列只留给 Django 自己的 `/admin`,本项目不读它。
23
27
  """
24
28
 
25
- list_display = ('username', 'work_id', 'name', 'is_staff', 'is_active', 'last_login')
26
- list_filter = ('is_staff', 'is_superuser', 'is_active')
29
+ list_display = ('username', 'work_id', 'name', 'is_active', 'last_login')
30
+ list_filter = ('is_active', 'is_superuser')
27
31
  search_fields = ('username', 'work_id', 'name')
28
32
  ordering = ('-date_joined',)
29
33
  readonly_fields = ('username', 'last_login', 'date_joined')
30
34
  fieldsets = (
31
35
  (None, {'fields': ('username', 'password')}),
32
36
  ('身份', {'fields': ('work_id', 'name', 'email')}),
33
- ('权限', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
37
+ ('权限', {'fields': ('is_active', 'is_superuser', 'groups', 'user_permissions')}),
34
38
  ('时间', {'fields': ('last_login', 'date_joined')}),
35
39
  )
36
40
 
37
41
 
42
+ @admin.register(LabullRole)
43
+ class LabullRoleAdmin(admin.ModelAdmin):
44
+ """项目角色:**码 → Django 组名**(框架不内置任何业务角色码;两个特殊角色由 SDK 的 0005 迁移种下)。
45
+
46
+ 🔴 它是"这个项目有哪些岗"的唯一真源:`flow.py` 里只引用 `role:<码>`,不写角色表。
47
+ 🔴 `is_admin`(管理员角色,组员就是平台管理员)与 `is_default_role`(默认角色,新用户自动加入)
48
+ **各恰好一条、不能同一行** —— 启动校验会拦;两条由 SDK 的 `0005` 迁移种下(`ADMIN` / `USER`)。
49
+ 🔴 组要先在「组」里建好 —— 这里只做配对,**不建组、不建用户**。
50
+ """
51
+
52
+ list_display = ('code', 'name', 'group', 'is_admin', 'is_default_role')
53
+ list_filter = ('is_admin', 'is_default_role')
54
+ search_fields = ('code', 'name', 'group')
55
+ ordering = ('code',)
56
+
57
+
38
58
  @admin.register(MagicBlock)
39
59
  class MagicBlockAdmin(admin.ModelAdmin):
40
60
  """功能块:看状态、筛 —— **改代码不在这里**(见文件头)。"""
@@ -10,7 +10,7 @@ class LabullConfig(AppConfig):
10
10
  (`Accessing the database during app initialization is discouraged`),而且新项目第一次
11
11
  `migrate` 时表还不存在 —— 那时查库会让**整个命令**起不来。
12
12
 
13
- 🔴 启动时要做的两件事(重设管理员名单 → 全量体检)在 `wsgi.py` 里一行显式调用:
13
+ 🔴 启动时要做的三件事(校验角色 → 重设管理员名单 → 全量体检)在 `wsgi.py` 里一行显式调用:
14
14
  ```python
15
15
  from labull_framework.magic_block.health import startup_once
16
16
  startup_once()
@@ -29,5 +29,9 @@ class LabullConfig(AppConfig):
29
29
  # 延迟 import:此刻 app 注册表已经就绪,但避免模块级 import 造成加载顺序问题。
30
30
  # 钩子是"库自己正常工作"的一部分(体检库里存着的块),**不是**对使用者项目的检查。
31
31
  from .magic_block import health
32
+ from .roles import install_check
32
33
 
33
34
  health.install_hook()
35
+ # 🔴 只**注册**一条 system check(不在这里查库):`manage.py check` / `runserver` 会在
36
+ # 真正查库之前把"两个特殊角色配错了"报出来;表还没迁移时那条 check 自己会跳过。
37
+ install_check()
@@ -5,8 +5,12 @@
5
5
  所以这个类**不碰会话**、也**不校验 token**(验签是 `sso.py` 的事,视图验完才把 claims 送进来)。
6
6
 
7
7
  🔴 **权限一个字都不给**:Django 有 `is_staff` / `is_superuser` / `Group` / `Permission`,
8
- 登录这件事只负责"你是谁";"你能干什么"由管理员在 admin 里给。
9
- 所以新建的用户三个布尔位就是 `is_staff=False` / `is_superuser=False` / `is_active=True`。
8
+ 登录这件事只负责"你是谁";"你能干什么"由**项目角色表**(`LabullRole`)与 Django 的组决定。
9
+ 新建的用户布尔位是 `is_staff=False` / `is_superuser=False` / `is_active=True`(`is_staff` 只留给
10
+ Django 自己的 `/admin`,平台的管理员看**管理员角色**)。
11
+ 🔴 **新用户默认加入默认角色**(`LabullRole.is_default_role` 那一行的组):这是"新来的人至少有个身份"
12
+ 的唯一入口;工号在 `BOOTSTRAP_ADMIN_WORK_IDS` 里的新同事还会顺手进**管理员角色**的组。
13
+ 老用户的组与权限**不动**(归管理员)。
10
14
  """
11
15
 
12
16
  from __future__ import annotations
@@ -28,10 +32,46 @@ NO_WORK_ID_TEXT = (
28
32
  )
29
33
 
30
34
  #: 老用户每次登录只允许被刷新的两列。🔴 抄进 `update_fields` 的那一份必须是**白名单**:
31
- #: 一旦顺手写成"全部字段",管理员在 admin 里设的 `is_staff` / 组 / 权限就会被一次登录冲掉。
35
+ #: 一旦顺手写成"全部字段",管理员在 admin 里设的组 / 权限就会被一次登录冲掉。
32
36
  UPDATABLE_FIELDS: tuple[str, ...] = ('name', 'work_id')
33
37
 
34
38
 
39
+ def _join_group(user, name: str) -> None:
40
+ """把这个用户加进某个组(组不存在就建);失败只记日志 —— 加组不该让人登不进来。"""
41
+ if not name:
42
+ return
43
+ from django.contrib.auth.models import Group
44
+
45
+ try:
46
+ group, _created = Group.objects.get_or_create(name=name)
47
+ user.groups.add(group)
48
+ except Exception as error: # noqa: BLE001
49
+ logger.error('新用户加入组「%s」失败(不影响登录):%s', name, error)
50
+
51
+
52
+ def _join_default_role(user) -> None:
53
+ """把这个**新用户**加进默认角色的组(没有默认角色 ⇒ 什么都不做)。
54
+
55
+ 🔴 只对**新建**的用户调:老用户的组归管理员,登录不许重排它。
56
+ """
57
+ from ..roles import default_role_group
58
+
59
+ _join_group(user, default_role_group())
60
+
61
+
62
+ def _maybe_join_admin_role(user) -> None:
63
+ """工号在 `BOOTSTRAP_ADMIN_WORK_IDS` 名单里 ⇒ 顺手加进**管理员角色**的组。
64
+
65
+ 🔴 启动时的 `sync_admin_work_ids()` 只管库里已经有的人;这是"名单里那个新同事
66
+ 第一次登录就已经是管理员"的那一半(同一个解析函数,名单不会两处不一致)。
67
+ """
68
+ from ..roles import admin_role_group
69
+ from .bootstrap import configured_admin_work_ids
70
+
71
+ if str(user.work_id or '') and str(user.work_id) in set(configured_admin_work_ids()):
72
+ _join_group(user, admin_role_group())
73
+
74
+
35
75
  class LoginRejected(Exception):
36
76
  """这次登录不放行,**一个字都不落库**,报文直接给用户看(视图回 401)。"""
37
77
 
@@ -119,7 +159,7 @@ class OidcBackend:
119
159
  current = user_model.objects.filter(username=sub).first()
120
160
  name = _text(data.get('name')) or sub
121
161
  if current is not None:
122
- # 🔴 老用户只刷新这两列:`is_staff` / `is_superuser` / 组与权限归管理员,
162
+ # 🔴 老用户只刷新这两列:组 / 权限 / is_superuser 归管理员,
123
163
  # 密码根本没有(见 `set_unusable_password`)。
124
164
  current.name = name
125
165
  current.work_id = work_id
@@ -133,6 +173,8 @@ class OidcBackend:
133
173
  user = user_model(username=sub, name=name, work_id=work_id)
134
174
  user.set_unusable_password()
135
175
  user.save()
176
+ _join_default_role(user)
177
+ _maybe_join_admin_role(user)
136
178
  return user
137
179
  except IntegrityError:
138
180
  # 并发首登:另一个请求刚插进去。重查拿**赢家写下的那一行** ——