labull-framework 1.4.1__tar.gz → 3.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 (69) hide show
  1. {labull_framework-1.4.1/labull_framework.egg-info → labull_framework-3.0.0}/PKG-INFO +3 -2
  2. {labull_framework-1.4.1 → labull_framework-3.0.0}/README-pypi.md +206 -205
  3. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/admin.py +11 -3
  4. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/apps.py +35 -33
  5. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/auth/backend.py +8 -4
  6. labull_framework-3.0.0/labull_framework/auth/bootstrap.py +99 -0
  7. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/auth/views.py +8 -2
  8. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/canvas/data.py +93 -31
  9. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/canvas/gitinfo.py +3 -2
  10. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/canvas/shape_ast.py +3 -2
  11. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/env.py +1 -1
  12. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/flow.py +228 -178
  13. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/devkit.py +21 -17
  14. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/dispatch.py +4 -2
  15. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/export.py +9 -2
  16. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/health.py +6 -4
  17. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/sdk.py +8 -4
  18. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/types/workflow.py +17 -22
  19. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/views.py +20 -16
  20. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/management/commands/labull_blocktype.py +19 -11
  21. labull_framework-3.0.0/labull_framework/migrations/0005_labullrole.py +62 -0
  22. labull_framework-3.0.0/labull_framework/migrations/0006_default_groups_permissions.py +142 -0
  23. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/models.py +15 -2
  24. labull_framework-3.0.0/labull_framework/permissions.py +101 -0
  25. {labull_framework-1.4.1 → labull_framework-3.0.0/labull_framework.egg-info}/PKG-INFO +3 -2
  26. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework.egg-info/SOURCES.txt +3 -0
  27. {labull_framework-1.4.1 → labull_framework-3.0.0}/pyproject.toml +90 -90
  28. labull_framework-1.4.1/labull_framework/auth/bootstrap.py +0 -75
  29. {labull_framework-1.4.1 → labull_framework-3.0.0}/LICENSE +0 -0
  30. {labull_framework-1.4.1 → labull_framework-3.0.0}/MANIFEST.in +0 -0
  31. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/__init__.py +0 -0
  32. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/auth/__init__.py +0 -0
  33. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/auth/sso.py +0 -0
  34. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/canvas/__init__.py +0 -0
  35. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/canvas/static/canvas.css +0 -0
  36. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/canvas/static/canvas.js +0 -0
  37. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/canvas/static/page.html +0 -0
  38. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/canvas/views.py +0 -0
  39. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/jsonio.py +0 -0
  40. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/__init__.py +0 -0
  41. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/blockcheck.py +0 -0
  42. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/blockpackage.py +0 -0
  43. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/blocktypes.py +0 -0
  44. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/deepcheck.py +0 -0
  45. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/manifest.py +0 -0
  46. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/parallel.py +0 -0
  47. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/registry.py +0 -0
  48. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/requires.py +0 -0
  49. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/service.py +0 -0
  50. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/subproc.py +0 -0
  51. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/types/__init__.py +0 -0
  52. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/types/plain.py +0 -0
  53. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/magic_block/validate.py +0 -0
  54. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/management/__init__.py +0 -0
  55. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/management/commands/__init__.py +0 -0
  56. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/management/commands/labull_contract.py +0 -0
  57. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/management/commands/labull_requires.py +0 -0
  58. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/migrations/0001_initial.py +0 -0
  59. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/migrations/0002_remove_magicblock_prev_files_and_more.py +0 -0
  60. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/migrations/0003_magicblock_tags.py +0 -0
  61. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/migrations/0004_magicblock_detail.py +0 -0
  62. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/migrations/__init__.py +0 -0
  63. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/settings.py +0 -0
  64. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/urls.py +0 -0
  65. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework/virtual.py +0 -0
  66. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework.egg-info/dependency_links.txt +0 -0
  67. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework.egg-info/requires.txt +0 -0
  68. {labull_framework-1.4.1 → labull_framework-3.0.0}/labull_framework.egg-info/top_level.txt +0 -0
  69. {labull_framework-1.4.1 → labull_framework-3.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: 3.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
+ | **谁是管理员** | 判据是**权限** `labull_framework.edit_magic_block`(不是组名、不是 `is_staff`)。配 `BOOTSTRAP_ADMIN_WORK_IDS=工号,工号` —— 它是**声明式**的名单:启动时名单里的人加进**持有那个权限的组**(按权限找组,改组名不影响),**名单之外的一律摘掉**(没配就是"一个管理员都没有")。它不碰 `is_superuser`,所以别拿超管当后门 |
97
+ | **默认组(权限包)** | **默认就有**:SDK 的 `0006` 迁移建「平台管理员」(`edit_magic_block` / `manage_users` / `view_magicblock`)与「普通用户」(`view_magicblock`)两个组,**只建组和权限、不加人**。组名只是标签,判据永远是权限;新用户默认进哪个组由**项目自己的 `post_save`** 决定(框架不装) |
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
+ | **谁是管理员** | 判据是**权限** `labull_framework.edit_magic_block`(不是组名、不是 `is_staff`)。配 `BOOTSTRAP_ADMIN_WORK_IDS=工号,工号` —— 它是**声明式**的名单:启动时名单里的人加进**持有那个权限的组**(按权限找组,改组名不影响),**名单之外的一律摘掉**(没配就是"一个管理员都没有")。它不碰 `is_superuser`,所以别拿超管当后门 |
69
+ | **默认组(权限包)** | **默认就有**:SDK 的 `0006` 迁移建「平台管理员」(`edit_magic_block` / `manage_users` / `view_magicblock`)与「普通用户」(`view_magicblock`)两个组,**只建组和权限、不加人**。组名只是标签,判据永远是权限;新用户默认进哪个组由**项目自己的 `post_save`** 决定(框架不装) |
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,9 @@
1
1
  """Django admin 的注册处 —— 这个库**唯一**的管理界面。
2
2
 
3
3
  🔴 它就是 Django 原生的那一套:用户 / 组 / 权限的日常维护白拿,我们不写任何自己的管理页面。
4
+ 🔴 **授权只看 `Permission`**(`app_label.codename`):组只是权限包("组"里打包权限、"用户"里加组);
5
+ 平台自己的两个权限住在 `MagicBlock.Meta.permissions`(`edit_magic_block` / `manage_users`)。
6
+ 两个默认组(`平台管理员` / `普通用户`)由迁移建好 —— 组名只是标签,代码不按它判断任何事。
4
7
  🔴 **块的代码在 admin 里只读**:它只能从 magic block 控制台写进去,因为那里有"保存即校验"那一关
5
8
  (`ast` + 子进程试加载)。admin 里放开编辑表单,就等于从 Django 的默认表单往库里塞
6
9
  **未经校验、之后会被 `exec` 执行**的代码,把"体检不过就停用"这套机制绕开。
@@ -20,17 +23,22 @@ class UserAdmin(admin.ModelAdmin):
20
23
 
21
24
  ⚠️ `username` 装的是 Authing 的 `sub`(登录时同步进来的),所以它**只读** ——
22
25
  改它等于把这一行与那个身份切开。
26
+ 🔴 **"是不是平台管理员" = 有没有 `labull_framework.edit_magic_block`**(`permissions.is_admin()`)——
27
+ 直接给在这个人身上、或者他所在的某个权限包里都算。这里不摆 `is_staff`:
28
+ 那一列只留给 Django 自己的 `/admin`,平台不读它。
29
+ 🔴 `groups` / `user_permissions` 两栏就是全部授权入口(组是权限包,权限码才是契约)。
30
+ 新用户**不自动进任何组**:那是项目的数据/策略(框架不装 `post_save`)。
23
31
  """
24
32
 
25
- list_display = ('username', 'work_id', 'name', 'is_staff', 'is_active', 'last_login')
26
- list_filter = ('is_staff', 'is_superuser', 'is_active')
33
+ list_display = ('username', 'work_id', 'name', 'is_active', 'last_login')
34
+ list_filter = ('is_active', 'is_superuser')
27
35
  search_fields = ('username', 'work_id', 'name')
28
36
  ordering = ('-date_joined',)
29
37
  readonly_fields = ('username', 'last_login', 'date_joined')
30
38
  fieldsets = (
31
39
  (None, {'fields': ('username', 'password')}),
32
40
  ('身份', {'fields': ('work_id', 'name', 'email')}),
33
- ('权限', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
41
+ ('权限', {'fields': ('is_active', 'is_superuser', 'groups', 'user_permissions')}),
34
42
  ('时间', {'fields': ('last_login', 'date_joined')}),
35
43
  )
36
44
 
@@ -1,33 +1,35 @@
1
- from __future__ import annotations
2
-
3
- from django.apps import AppConfig
4
-
5
-
6
- class LabullConfig(AppConfig):
7
- """这个 app 的登记项。
8
-
9
- 🔴 `ready()` 里**只挂一个钩子,不碰数据库**:Django 对"在 `ready()` 里查库"会明确警告
10
- (`Accessing the database during app initialization is discouraged`),而且新项目第一次
11
- `migrate` 时表还不存在 —— 那时查库会让**整个命令**起不来。
12
-
13
- 🔴 启动时要做的两件事(重设管理员名单 → 全量体检)在 `wsgi.py` 里一行显式调用:
14
- ```python
15
- from labull_framework.magic_block.health import startup_once
16
- startup_once()
17
- ```
18
- 那一行是**契约**(函数名与签名都不要改):`runserver` / gunicorn / uwsgi / ASGI
19
- 都从那里进来,所以它天然是"每个进程一次"的地方。
20
- ⚠️ 它**不是** `post_migrate` 的替代:那个只在迁移之后发(管"库刚被改过"),
21
- 而启动体检只有 `startup_once()` 能做。
22
- """
23
-
24
- default_auto_field = 'django.db.models.BigAutoField'
25
- name = 'labull_framework'
26
- verbose_name = 'Labull Framework'
27
-
28
- def ready(self) -> None:
29
- # 延迟 import:此刻 app 注册表已经就绪,但避免模块级 import 造成加载顺序问题。
30
- # 钩子是"库自己正常工作"的一部分(体检库里存着的块),**不是**对使用者项目的检查。
31
- from .magic_block import health
32
-
33
- health.install_hook()
1
+ from __future__ import annotations
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class LabullConfig(AppConfig):
7
+ """这个 app 的登记项。
8
+
9
+ 🔴 `ready()` 里**只挂一个钩子,不碰数据库**:Django 对"在 `ready()` 里查库"会明确警告
10
+ (`Accessing the database during app initialization is discouraged`),而且新项目第一次
11
+ `migrate` 时表还不存在 —— 那时查库会让**整个命令**起不来。
12
+ 🔴 `ready()` 里**不装任何 `post_save` / `user_logged_in` 接收器**:框架不给新用户加组
13
+ (那是项目的数据/策略,示例项目给了一份可复制的实现:`backend/example/canvasdemo/signals.py`)。
14
+
15
+ 🔴 启动时要做的两件事(重设管理员名单 → 全量体检)在 `wsgi.py` 里一行显式调用:
16
+ ```python
17
+ from labull_framework.magic_block.health import startup_once
18
+ startup_once()
19
+ ```
20
+ 那一行是**契约**(函数名与签名都不要改):`runserver` / gunicorn / uwsgi / ASGI
21
+ 都从那里进来,所以它天然是"每个进程一次"的地方。
22
+ ⚠️ 它**不是** `post_migrate` 的替代:那个只在迁移之后发(管"库刚被改过"),
23
+ 而启动体检只有 `startup_once()` 能做。
24
+ """
25
+
26
+ default_auto_field = 'django.db.models.BigAutoField'
27
+ name = 'labull_framework'
28
+ verbose_name = 'Labull Framework'
29
+
30
+ def ready(self) -> None:
31
+ # 延迟 import:此刻 app 注册表已经就绪,但避免模块级 import 造成加载顺序问题。
32
+ # 钩子是"库自己正常工作"的一部分(体检库里存着的块),**不是**对使用者项目的检查。
33
+ from .magic_block import health
34
+
35
+ health.install_hook()
@@ -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
+ 登录这件事只负责"你是谁";"你能干什么"只看 **`Permission`**(`app_label.codename`),
9
+ 组只是权限包。新建的用户布尔位是 `is_staff=False` / `is_superuser=False` / `is_active=True`
10
+ (`is_staff` 只留给 Django 自己的 `/admin`;平台的管理员看 `edit_magic_block` 这个权限)。
11
+ 🔴 **新用户不自动进任何组**(框架不装 `post_save` / `user_logged_in`):新来的人默认属于哪个组是
12
+ **项目的数据/策略**,由项目自己决定(示例项目给了一份可复制的实现:
13
+ `backend/example/canvasdemo/signals.py`)。老用户的组与权限**不动**(归管理员)。
10
14
  """
11
15
 
12
16
  from __future__ import annotations
@@ -28,7 +32,7 @@ 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
 
@@ -119,7 +123,7 @@ class OidcBackend:
119
123
  current = user_model.objects.filter(username=sub).first()
120
124
  name = _text(data.get('name')) or sub
121
125
  if current is not None:
122
- # 🔴 老用户只刷新这两列:`is_staff` / `is_superuser` / 组与权限归管理员,
126
+ # 🔴 老用户只刷新这两列:组 / 权限 / is_superuser 归管理员,
123
127
  # 密码根本没有(见 `set_unusable_password`)。
124
128
  current.name = name
125
129
  current.work_id = work_id