labull-framework 2.0.0__tar.gz → 3.0.1__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.
- {labull_framework-2.0.0/labull_framework.egg-info → labull_framework-3.0.1}/PKG-INFO +3 -3
- {labull_framework-2.0.0 → labull_framework-3.0.1}/README-pypi.md +2 -2
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/admin.py +9 -21
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/apps.py +35 -37
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/auth/backend.py +6 -44
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/auth/bootstrap.py +26 -25
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/auth/views.py +4 -4
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/canvas/data.py +93 -31
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/canvas/gitinfo.py +3 -2
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/canvas/shape_ast.py +3 -2
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/env.py +1 -1
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/flow.py +144 -114
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/devkit.py +19 -16
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/dispatch.py +1 -4
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/export.py +9 -2
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/health.py +5 -12
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/sdk.py +5 -5
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/types/workflow.py +8 -8
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/views.py +22 -19
- labull_framework-3.0.1/labull_framework/migrations/0006_default_groups_permissions.py +142 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/models.py +16 -80
- labull_framework-3.0.1/labull_framework/permissions.py +86 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1/labull_framework.egg-info}/PKG-INFO +3 -3
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework.egg-info/SOURCES.txt +2 -1
- {labull_framework-2.0.0 → labull_framework-3.0.1}/pyproject.toml +90 -90
- labull_framework-2.0.0/labull_framework/roles.py +0 -189
- {labull_framework-2.0.0 → labull_framework-3.0.1}/LICENSE +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/MANIFEST.in +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/__init__.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/auth/__init__.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/auth/sso.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/canvas/__init__.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/canvas/static/canvas.css +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/canvas/static/canvas.js +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/canvas/static/page.html +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/canvas/views.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/jsonio.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/__init__.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/blockcheck.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/blockpackage.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/blocktypes.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/deepcheck.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/manifest.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/parallel.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/registry.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/requires.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/service.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/subproc.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/types/__init__.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/types/plain.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/validate.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/management/__init__.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/management/commands/__init__.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/management/commands/labull_blocktype.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/management/commands/labull_contract.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/management/commands/labull_requires.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0001_initial.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0002_remove_magicblock_prev_files_and_more.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0003_magicblock_tags.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0004_magicblock_detail.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0005_labullrole.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/migrations/__init__.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/settings.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/urls.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework/virtual.py +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework.egg-info/dependency_links.txt +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework.egg-info/requires.txt +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/labull_framework.egg-info/top_level.txt +0 -0
- {labull_framework-2.0.0 → labull_framework-3.0.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labull-framework
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.1
|
|
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,8 +93,8 @@ CORS_ORIGIN=http://localhost:5173
|
|
|
93
93
|
|
|
94
94
|
| 想要什么 | 怎么做 |
|
|
95
95
|
|---|---|
|
|
96
|
-
| **谁是管理员** |
|
|
97
|
-
|
|
|
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`** 决定(框架不装) |
|
|
98
98
|
| **看数据模型那一页** | 设 `LABULL_SCHEMA_CANVAS=on`,浏览器打开 `/` |
|
|
99
99
|
| **块的依赖声明不许"用了却没声明"** | 它是**默认**行为(`LABULL_REQUIRES=error`,1.0.1 起):代码里用了没声明的东西 ⇒ 拦住保存。⚠️ 升级前先在项目里跑一遍 `python manage.py labull_requires --write` 把存量块扫得到的声明补进库;`warn`(只提示不拦)与 `off`(整段不跑)都要**显式**写 |
|
|
100
100
|
| **导出一份模型形状** | `python manage.py labull_contract --out .labull/contract.json` |
|
|
@@ -65,8 +65,8 @@ CORS_ORIGIN=http://localhost:5173
|
|
|
65
65
|
|
|
66
66
|
| 想要什么 | 怎么做 |
|
|
67
67
|
|---|---|
|
|
68
|
-
| **谁是管理员** |
|
|
69
|
-
|
|
|
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
70
|
| **看数据模型那一页** | 设 `LABULL_SCHEMA_CANVAS=on`,浏览器打开 `/` |
|
|
71
71
|
| **块的依赖声明不许"用了却没声明"** | 它是**默认**行为(`LABULL_REQUIRES=error`,1.0.1 起):代码里用了没声明的东西 ⇒ 拦住保存。⚠️ 升级前先在项目里跑一遍 `python manage.py labull_requires --write` 把存量块扫得到的声明补进库;`warn`(只提示不拦)与 `off`(整段不跑)都要**显式**写 |
|
|
72
72
|
| **导出一份模型形状** | `python manage.py labull_contract --out .labull/contract.json` |
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Django admin 的注册处 —— 这个库**唯一**的管理界面。
|
|
2
2
|
|
|
3
3
|
🔴 它就是 Django 原生的那一套:用户 / 组 / 权限的日常维护白拿,我们不写任何自己的管理页面。
|
|
4
|
-
🔴
|
|
5
|
-
|
|
4
|
+
🔴 **授权只看 `Permission`**(`app_label.codename`):组只是权限包("组"里打包权限、"用户"里加组);
|
|
5
|
+
平台自己的两个权限住在 `MagicBlock.Meta.permissions`(`edit_magic_block` / `manage_users`)。
|
|
6
|
+
两个默认组(`平台管理员` / `普通用户`)由迁移建好 —— 组名只是标签,代码不按它判断任何事。
|
|
6
7
|
🔴 **块的代码在 admin 里只读**:它只能从 magic block 控制台写进去,因为那里有"保存即校验"那一关
|
|
7
8
|
(`ast` + 子进程试加载)。admin 里放开编辑表单,就等于从 Django 的默认表单往库里塞
|
|
8
9
|
**未经校验、之后会被 `exec` 执行**的代码,把"体检不过就停用"这套机制绕开。
|
|
@@ -13,7 +14,7 @@ from __future__ import annotations
|
|
|
13
14
|
|
|
14
15
|
from django.contrib import admin
|
|
15
16
|
|
|
16
|
-
from .models import
|
|
17
|
+
from .models import MagicBlock, User
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@admin.register(User)
|
|
@@ -22,8 +23,11 @@ class UserAdmin(admin.ModelAdmin):
|
|
|
22
23
|
|
|
23
24
|
⚠️ `username` 装的是 Authing 的 `sub`(登录时同步进来的),所以它**只读** ——
|
|
24
25
|
改它等于把这一行与那个身份切开。
|
|
25
|
-
🔴
|
|
26
|
-
|
|
26
|
+
🔴 **"能不能编辑块" = 有没有 `labull_framework.edit_magic_block`**(`permissions.EDIT_PERMISSION`)——
|
|
27
|
+
直接给在这个人身上、或者他所在的某个权限包里都算。这里不摆 `is_staff`:
|
|
28
|
+
那一列只留给 Django 自己的 `/admin`,平台不读它。
|
|
29
|
+
🔴 `groups` / `user_permissions` 两栏就是全部授权入口(组是权限包,权限码才是契约)。
|
|
30
|
+
新用户**不自动进任何组**:那是项目的数据/策略(框架不装 `post_save`)。
|
|
27
31
|
"""
|
|
28
32
|
|
|
29
33
|
list_display = ('username', 'work_id', 'name', 'is_active', 'last_login')
|
|
@@ -39,22 +43,6 @@ class UserAdmin(admin.ModelAdmin):
|
|
|
39
43
|
)
|
|
40
44
|
|
|
41
45
|
|
|
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
|
-
|
|
58
46
|
@admin.register(MagicBlock)
|
|
59
47
|
class MagicBlockAdmin(admin.ModelAdmin):
|
|
60
48
|
"""功能块:看状态、筛 —— **改代码不在这里**(见文件头)。"""
|
|
@@ -1,37 +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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# 真正查库之前把"两个特殊角色配错了"报出来;表还没迁移时那条 check 自己会跳过。
|
|
37
|
-
install_check()
|
|
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,12 +5,12 @@
|
|
|
5
5
|
所以这个类**不碰会话**、也**不校验 token**(验签是 `sso.py` 的事,视图验完才把 claims 送进来)。
|
|
6
6
|
|
|
7
7
|
🔴 **权限一个字都不给**:Django 有 `is_staff` / `is_superuser` / `Group` / `Permission`,
|
|
8
|
-
登录这件事只负责"你是谁";"你能干什么"
|
|
9
|
-
|
|
10
|
-
Django 自己的 `/admin
|
|
11
|
-
🔴
|
|
12
|
-
|
|
13
|
-
|
|
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`)。老用户的组与权限**不动**(归管理员)。
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
@@ -36,42 +36,6 @@ NO_WORK_ID_TEXT = (
|
|
|
36
36
|
UPDATABLE_FIELDS: tuple[str, ...] = ('name', 'work_id')
|
|
37
37
|
|
|
38
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
|
-
|
|
75
39
|
class LoginRejected(Exception):
|
|
76
40
|
"""这次登录不放行,**一个字都不落库**,报文直接给用户看(视图回 401)。"""
|
|
77
41
|
|
|
@@ -173,8 +137,6 @@ class OidcBackend:
|
|
|
173
137
|
user = user_model(username=sub, name=name, work_id=work_id)
|
|
174
138
|
user.set_unusable_password()
|
|
175
139
|
user.save()
|
|
176
|
-
_join_default_role(user)
|
|
177
|
-
_maybe_join_admin_role(user)
|
|
178
140
|
return user
|
|
179
141
|
except IntegrityError:
|
|
180
142
|
# 并发首登:另一个请求刚插进去。重查拿**赢家写下的那一行** ——
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"""启动时按 `BOOTSTRAP_ADMIN_WORK_IDS` **重设**谁是管理员 —— **这是"管理员名单"的唯一入口**。
|
|
2
2
|
|
|
3
|
-
🔴
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
🔴 判据是**声明式**的:那份清单**就是**管理员名单。每次启动都拿它和**那个装管理员权限的组**
|
|
4
|
+
(`edit_magic_block` 所在的权限包)对一遍 —— 名单里的人加进那个组,
|
|
5
|
+
**名单之外的(以及没有工号的)一律摘掉**。于是"某人离职 / 不该再当管理员"这件事只需要从 `.env`
|
|
6
|
+
里删掉一个工号 + 重启,不必记得去点。
|
|
7
|
+
🔴 "算不算管理员"的唯一判据是 **有没有 `labull_framework.edit_magic_block` 权限**
|
|
8
|
+
(`EDIT_PERMISSION in permissions_of(user)`)—— **不是**组名、**不是** Django 的 `is_staff`
|
|
9
|
+
(那一列只留给 Django 自己的 `/admin`)。这里之所以要找一个"组",只是因为**写**的时候要有个落点;
|
|
10
|
+
找法按**权限**找(`permissions.group_holding(EDIT_PERMISSION)`),所以项目把那个组改名也不影响这份名单。
|
|
11
|
+
🔴 **没配 / 是空 ⇒ 把所有人的管理员权限摘掉**("没配"的语义就是"管理员一个都没有")。
|
|
9
12
|
⚠️ 这是刻意选的 **fail-closed**:`.env` 少写一行、部署时忘了带这个变量,后果是**一个管理员都没有**
|
|
10
13
|
—— 而不是"上一个被删掉的人悄悄留着权限"。要救回来:把那一行补回去重启。
|
|
11
|
-
⚠️ 反过来说:**它不碰 `is_superuser
|
|
14
|
+
⚠️ 反过来说:**它不碰 `is_superuser` / 直接给某个人的权限**(那些是另一层,不归这份名单管)。
|
|
12
15
|
🔴 清单超过 `MAX_WORK_IDS` 时**只取前 200 个**(防误粘)—— 于是第 201 个之后的人会被停掉,
|
|
13
16
|
所以那一条 warning 必须显式说出来(不然症状是"名单明明写了,他就是进不去")。
|
|
17
|
+
🔴 框架**不装 `post_save`**:这份名单只在启动 / 迁移之后同步一次。名单里新加的人**下一次重启**
|
|
18
|
+
才上岗("登录时顺手加组"那条路 3.0.0 起没有了 —— 组归管理员/项目策略)。
|
|
14
19
|
"""
|
|
15
20
|
|
|
16
21
|
from __future__ import annotations
|
|
@@ -19,6 +24,7 @@ import logging
|
|
|
19
24
|
|
|
20
25
|
from ..env import read_env
|
|
21
26
|
from ..models import User
|
|
27
|
+
from ..permissions import EDIT_PERMISSION, group_holding
|
|
22
28
|
|
|
23
29
|
logger = logging.getLogger(__name__)
|
|
24
30
|
|
|
@@ -34,18 +40,18 @@ def _raw_work_ids() -> list[str]:
|
|
|
34
40
|
def configured_admin_work_ids() -> list[str]:
|
|
35
41
|
"""`BOOTSTRAP_ADMIN_WORK_IDS` 配的那份工号(去空白、截到上限;**不报警告**)。
|
|
36
42
|
|
|
37
|
-
🔴
|
|
38
|
-
|
|
43
|
+
🔴 只有这一处解析:谁要问"名单里有谁"就调它(另一处手写一遍的话,`.env` 里多一个空格
|
|
44
|
+
就会让两边的名单不一样)。
|
|
39
45
|
"""
|
|
40
46
|
return _raw_work_ids()[:MAX_WORK_IDS]
|
|
41
47
|
|
|
42
48
|
|
|
43
49
|
def sync_admin_work_ids() -> int:
|
|
44
|
-
"""把"
|
|
50
|
+
"""把"那个装管理员权限的组"的成员重设成"只有名单里的人",回这次**真的**改了几个人(上岗 + 下岗)。
|
|
45
51
|
|
|
46
52
|
🔴 **永不抛**:这是启动路径,库连不上 / 表还没迁移都只该在日志里留一句 ——
|
|
47
53
|
重设管理员失败没有理由让整个后端起不来。
|
|
48
|
-
🔴
|
|
54
|
+
🔴 只动**那一个组**:别的组一个字都不碰 —— 那些是项目自己的权限包。
|
|
49
55
|
🔴 判据是**名单**,不是"库里现在谁在组里":`work_id` 为 `NULL` 的行永远不在名单里
|
|
50
56
|
(SQL 的 `NULL IN (…)` / `NULL NOT IN (…)` 都是 unknown),所以"没有工号的人"会被摘掉 ——
|
|
51
57
|
这是对的(认人只认工号),但也意味着**没有工号的人不可能当管理员**。
|
|
@@ -61,23 +67,18 @@ def sync_admin_work_ids() -> int:
|
|
|
61
67
|
if not work_ids:
|
|
62
68
|
logger.warning(
|
|
63
69
|
'BOOTSTRAP_ADMIN_WORK_IDS 没配 / 是空的:按"管理员一个都没有"处理,'
|
|
64
|
-
'
|
|
70
|
+
'会把所有人的管理员权限摘掉。要指定管理员就在 .env 里写一行 '
|
|
65
71
|
'BOOTSTRAP_ADMIN_WORK_IDS=<工号>[,<工号>…]。'
|
|
66
72
|
)
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if not group_name:
|
|
74
|
-
# 启动校验(roles.validate_roles)会先把"没有管理员角色"拦下来;走到这里说明
|
|
75
|
-
# 这一步跑在它之前(例如某个自定义命令),不在启动路径上制造第二次失败。
|
|
76
|
-
logger.error('还没有管理员角色(迁移里的 ADMIN / 项目角色表):跳过管理员名单同步')
|
|
74
|
+
group = group_holding(EDIT_PERMISSION)
|
|
75
|
+
if group is None:
|
|
76
|
+
# 迁移会建"平台管理员"那个权限包;走到这里说明库还没迁移(或权限行还没建出来),
|
|
77
|
+
# 不在启动路径上制造第二次失败 —— 下一次启动/迁移之后它就有了。
|
|
78
|
+
logger.error('还没有装 %s 的组(权限包):跳过管理员名单同步', EDIT_PERMISSION)
|
|
77
79
|
return 0
|
|
78
80
|
|
|
79
81
|
try:
|
|
80
|
-
group, _created = Group.objects.get_or_create(name=group_name)
|
|
81
82
|
wanted = list(User.objects.filter(work_id__in=work_ids))
|
|
82
83
|
wanted_ids = {user.pk for user in wanted}
|
|
83
84
|
current_ids = set(group.user_set.values_list('pk', flat=True))
|
|
@@ -88,11 +89,11 @@ def sync_admin_work_ids() -> int:
|
|
|
88
89
|
if removed:
|
|
89
90
|
group.user_set.remove(*User.objects.filter(pk__in=removed))
|
|
90
91
|
except Exception as error: # noqa: BLE001 —— 见 docstring:启动路径永不抛
|
|
91
|
-
logger.error('按 BOOTSTRAP_ADMIN_WORK_IDS
|
|
92
|
+
logger.error('按 BOOTSTRAP_ADMIN_WORK_IDS 同步管理员失败(不影响启动):%s', error)
|
|
92
93
|
return 0
|
|
93
94
|
if added or removed:
|
|
94
95
|
logger.info(
|
|
95
|
-
'按 BOOTSTRAP_ADMIN_WORK_IDS
|
|
96
|
-
|
|
96
|
+
'按 BOOTSTRAP_ADMIN_WORK_IDS 同步管理员「%s」:加进 %s 个、摘掉 %s 个(名单里有 %s 个工号)',
|
|
97
|
+
group.name, len(added), len(removed), len(work_ids),
|
|
97
98
|
)
|
|
98
99
|
return len(added) + len(removed)
|
|
@@ -133,16 +133,16 @@ def user_to_dict(user) -> dict:
|
|
|
133
133
|
而模型是 `snake_case` ⇒ 两种形状之间的转换只该有**这一处**。
|
|
134
134
|
⚠️ `work_id` 可空就原样回 `None`,不兜成空串(空串会被当成"有个空工号")。
|
|
135
135
|
⚠️ 时间只有一处实现(`jsonio.iso_utc`):别再写第二份 `isoformat().replace(…)`。
|
|
136
|
-
🔴 `isAdmin`
|
|
136
|
+
🔴 **没有 `isAdmin` 这类派生位**:能做什么只看 `permissions`(`app_label.codename`,
|
|
137
|
+
直接给的 + 经组给的)。前端要判"能不能编辑块"就查
|
|
138
|
+
`permissions.includes('labull_framework.edit_magic_block')`。
|
|
139
|
+
🔴 `groups` 只是权限包的名字(只给人看/排查),**不要**拿组名做判断。
|
|
137
140
|
"""
|
|
138
|
-
from ..roles import is_admin
|
|
139
|
-
|
|
140
141
|
return {
|
|
141
142
|
'id': user.pk,
|
|
142
143
|
'username': user.get_username(),
|
|
143
144
|
'name': user.name,
|
|
144
145
|
'workId': user.work_id,
|
|
145
|
-
'isAdmin': is_admin(user),
|
|
146
146
|
'isSuperuser': user.is_superuser,
|
|
147
147
|
'isActive': user.is_active,
|
|
148
148
|
'groups': [group.name for group in user.groups.all()],
|
|
@@ -24,23 +24,42 @@ from .. import virtual as virtual_registry
|
|
|
24
24
|
#: 拖拽位置的落点(相对项目根)。🚫 不落盘也能用(没有这个目录就不持久化,不报错)。
|
|
25
25
|
LAYOUT_FILE = '.labull/schema-layout.json'
|
|
26
26
|
|
|
27
|
-
#:
|
|
28
|
-
#:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'django_session',
|
|
40
|
-
}
|
|
27
|
+
#: Django 自带的那几个模型在画布上归到**这一个模块**(`模块` 下拉框里那一项叫 `Django`)。
|
|
28
|
+
#: 🔴 3.0.0 起它们**也画**(用户要的:`labull_user ──groups──> auth_group ──permissions──> auth_permission`
|
|
29
|
+
#: 这张图要能一眼看全),但只**展示**、**不参与 git diff**(见 `_diffable()`):
|
|
30
|
+
#: 它们的源码住在装着的 Django 里,不存在"这个项目的上一版",比出来的只会是假差异。
|
|
31
|
+
BUILTIN_MODULE = 'Django'
|
|
32
|
+
|
|
33
|
+
#: Django 自带的抽象基类:画成 `Django` 模块里的**紫色虚拟卡片**(与 `WorkflowModel` 同一种样式)。
|
|
34
|
+
#: 🔴 **不画继承线**:它们不是"本项目的泛化",只是"这张表继承了 Django 的那一层"。
|
|
35
|
+
BUILTIN_VIRTUALS: tuple[str, ...] = (
|
|
36
|
+
'django.contrib.auth.models.AbstractUser',
|
|
37
|
+
'django.contrib.auth.base_user.AbstractBaseUser',
|
|
38
|
+
'django.contrib.auth.models.PermissionsMixin',
|
|
41
39
|
)
|
|
42
40
|
|
|
43
41
|
|
|
42
|
+
def _is_builtin(model: Any) -> bool:
|
|
43
|
+
"""这张模型是不是 **Django 自己的**(`django.contrib.*` 那几张)。
|
|
44
|
+
|
|
45
|
+
🔴 判据是**模块路径**(`model.__module__` 以 `django.` 开头),不是 app 标签、更不是表名:
|
|
46
|
+
`auth.Group` / `admin.LogEntry` / `contenttypes.ContentType` / `sessions.Session`
|
|
47
|
+
全都住在 Django 里,而它们的 app 标签各是 `auth` / `admin` / …(一张名单迟早漏)。
|
|
48
|
+
⚠️ 一律不抛:读不出来就当"不是内置"(宁可多画一张业务卡片,也不要把它藏了)。
|
|
49
|
+
"""
|
|
50
|
+
module = str(getattr(model, '__module__', '') or '')
|
|
51
|
+
return module == 'django' or module.startswith('django.')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _diffable(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
55
|
+
"""从一份形状里剔掉**内置卡片**(`builtin`):它们只展示、不参与 git diff。
|
|
56
|
+
|
|
57
|
+
🔴 只有这一处判据:`payload()` 算表 / 虚拟类为类的三态之前都先过它 ——
|
|
58
|
+
内置卡片的"上一版"在任何人手里都不存在(Django 的源码不在这个项目的 git 里)。
|
|
59
|
+
"""
|
|
60
|
+
return [item for item in items if not item.get('builtin')]
|
|
61
|
+
|
|
62
|
+
|
|
44
63
|
def enabled() -> bool:
|
|
45
64
|
"""那一页开不开:`on` 常开;`auto` 只在 `DEBUG` 时开;其余(含默认 `off`)不开。"""
|
|
46
65
|
switch = env.read_env('LABULL_SCHEMA_CANVAS').lower()
|
|
@@ -119,6 +138,28 @@ def _virtual_models() -> list[Any]:
|
|
|
119
138
|
return out
|
|
120
139
|
|
|
121
140
|
|
|
141
|
+
def _builtin_virtuals() -> list[Any]:
|
|
142
|
+
"""Django 自带的那几个**抽象基类**(`AbstractUser` / `AbstractBaseUser` / `PermissionsMixin`)。
|
|
143
|
+
|
|
144
|
+
🔴 它们**不是** `@virtual_class` 打的标记(那要求"本项目自己的类"),所以不进 `virtual` 登记表;
|
|
145
|
+
但对画布来说它们是同一类东西:**不建表的抽象模型** ⇒ 同一张紫色卡片,只是归到 `Django` 模块。
|
|
146
|
+
🔴 **不画继承线**:`User` 表上不会因为继承了它们而多出泛化标记(`virtualParents` 只认登记表:
|
|
147
|
+
见 `_virtual_ancestors_of()`)—— "Django 的那一层"不该混进项目的泛化关系里。
|
|
148
|
+
⚠️ 拿不到(Django 版本不同 / 模块路径变了)就跳过那一条,不抛。
|
|
149
|
+
"""
|
|
150
|
+
import importlib
|
|
151
|
+
|
|
152
|
+
out: list[Any] = []
|
|
153
|
+
for path in BUILTIN_VIRTUALS:
|
|
154
|
+
module_name, _, class_name = path.rpartition('.')
|
|
155
|
+
try:
|
|
156
|
+
model = getattr(importlib.import_module(module_name), class_name)
|
|
157
|
+
except Exception: # noqa: BLE001 —— 见 docstring:拿不到就少一张卡片
|
|
158
|
+
continue
|
|
159
|
+
out.append(model)
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
|
|
122
163
|
def _table_of_model(model: Any) -> str:
|
|
123
164
|
"""一个模型的表名(`_meta.db_table`;拿不到回空串)—— 判"这张表画不画"用。
|
|
124
165
|
|
|
@@ -352,9 +393,9 @@ def m2m_through(field: Any) -> Any:
|
|
|
352
393
|
field 本身**,而它在 Django 里**没有 `through` 属性**(不是 None,是 AttributeError),
|
|
353
394
|
中间表挂在 `field.through` 上。
|
|
354
395
|
⚠️ 老代码写的是 `getattr(field.remote_field, 'through', None)`:反向那一侧拿到 `None`,
|
|
355
|
-
紧接着 `None._meta` 抛 `AttributeError` —— 而 `auth`
|
|
356
|
-
|
|
357
|
-
一起吞了,连报错都指不到真正的原因。
|
|
396
|
+
紧接着 `None._meta` 抛 `AttributeError` —— 而 `auth` 的那几张表当年不画(`SKIP_TABLES`,
|
|
397
|
+
3.0.0 已删:Django 自带的表现在也画)、平时又没人写过普通多对多,所以这个坑一直没露出来。
|
|
398
|
+
`getattr` 的默认值把 AttributeError 一起吞了,连报错都指不到真正的原因。
|
|
358
399
|
"""
|
|
359
400
|
for owner in (getattr(field, 'remote_field', None), field):
|
|
360
401
|
through = getattr(owner, 'through', None)
|
|
@@ -376,11 +417,14 @@ def shape() -> dict[str, list[dict[str, Any]]]:
|
|
|
376
417
|
"""
|
|
377
418
|
tables: list[dict[str, Any]] = []
|
|
378
419
|
for model in apps.get_models():
|
|
379
|
-
|
|
380
|
-
|
|
420
|
+
# 🔴 **一张都不跳**(3.0.0 起):Django 自带的那几张也画,只是归到 `Django` 模块、
|
|
421
|
+
# 并且不参与 git diff(见 `_card()` / `_diffable()`)。
|
|
381
422
|
tables.append(_card(model, str(model._meta.db_table), virtual=False))
|
|
382
423
|
tables.sort(key=lambda item: (item['module'], item['table']))
|
|
383
|
-
virtuals = [
|
|
424
|
+
virtuals = [
|
|
425
|
+
_card(model, str(model._meta.db_table), virtual=True)
|
|
426
|
+
for model in (list(_virtual_models()) + _builtin_virtuals())
|
|
427
|
+
]
|
|
384
428
|
virtuals.sort(key=lambda item: (item['module'], item['table']))
|
|
385
429
|
return {'tables': tables, 'virtuals': virtuals}
|
|
386
430
|
|
|
@@ -393,13 +437,18 @@ def _card(model: Any, table: str, *, virtual: bool) -> dict[str, Any]:
|
|
|
393
437
|
会变成一类只在虚拟卡片上出现的前端报错。
|
|
394
438
|
"""
|
|
395
439
|
fields, relations, enums = _fields_of(model)
|
|
440
|
+
builtin = _is_builtin(model)
|
|
396
441
|
return {
|
|
397
442
|
'name': model.__name__,
|
|
398
443
|
'table': table,
|
|
399
|
-
|
|
444
|
+
# 🔴 Django 自带的模型归到 `Django` 这一个模块(模块下拉框里那一项),别的用 app 标签。
|
|
445
|
+
'module': BUILTIN_MODULE if builtin else str(model._meta.app_label),
|
|
400
446
|
'verbose': _verbose(model),
|
|
401
447
|
'brief': _brief(model),
|
|
402
448
|
'briefLine': _brief_line(model),
|
|
449
|
+
# 🔴 **这张卡片是不是 Django 自带的**:只展示、不参与 git diff(见 `_diffable()`)。
|
|
450
|
+
# 前端可以不读它(模块名已经够分组了),但它是"为什么不带差异色"的书面答案。
|
|
451
|
+
'builtin': builtin,
|
|
403
452
|
# 🔴 这张表**自己定义**的那几个枚举类(类名 → 中文名 / 成员)。
|
|
404
453
|
# 它是**只增不减**的信息:画布不读它,导出(`devkit`)靠它写出 `Order.Status.OPEN`
|
|
405
454
|
# 那种写法。见 `_enums_of()` 顶上那段(为什么必须扫模型类体)。
|
|
@@ -477,14 +526,14 @@ def _fields_of(model: Any) -> tuple[list[dict[str, Any]], list[dict[str, Any]],
|
|
|
477
526
|
)
|
|
478
527
|
if reverse_rel:
|
|
479
528
|
target = field.related_model
|
|
480
|
-
# 🔴 **对端是
|
|
481
|
-
#
|
|
482
|
-
#
|
|
483
|
-
# 那一句)⇒
|
|
529
|
+
# 🔴 **对端是 Django 自己的模型时不补这条反向关联**(`auth_permission` 上的
|
|
530
|
+
# `group` / `user` 之类):它的正向声明住在 **Django 自己的源码**里,而"上一版"
|
|
531
|
+
# 那一份只扫项目与装着的第三方 app(`shape_ast._reverses()` 里
|
|
532
|
+
# `if target is None or not self._installed(...)` 那一句)⇒ 基线里**永远没有**
|
|
533
|
+
# 这一条,于是画布常年报一处假的"新增的关联"。
|
|
484
534
|
# 实测就是它:装了 `django.contrib.admin` 时用户表上那条 `labull_user.logentry`。
|
|
485
|
-
# ⚠️
|
|
486
|
-
|
|
487
|
-
if _table_of_model(target) in SKIP_TABLES:
|
|
535
|
+
# ⚠️ 内置卡片本身照样画(`Django` 模块);这里只管"别给反向关联造一份假差异"。
|
|
536
|
+
if _is_builtin(target):
|
|
488
537
|
continue
|
|
489
538
|
# 🔴 一对一反过来的**基数也是 `1`**:这一头只对应一行(`OneToOneRel.one_to_one`)。
|
|
490
539
|
# 报成 `*` 会让前端画出"一对多"那种端口,也与基线那一份(`back_card = '1'`)对不上
|
|
@@ -696,7 +745,12 @@ def payload() -> dict[str, Any]:
|
|
|
696
745
|
from . import gitinfo
|
|
697
746
|
|
|
698
747
|
current = shape()
|
|
699
|
-
|
|
748
|
+
# 🔴 **内置卡片只展示、不参与 diff**(`_diffable()`):`current['tables']` / `virtuals` 原样
|
|
749
|
+
# 发给前端画(`Django` 模块那几张),而下面每一条差异函数吃的都是剔掉 `builtin` 的那一份。
|
|
750
|
+
# 它们的"上一版"不存在于这个项目的 git 里 —— 比进去只会给每张内置卡片配一条假差异。
|
|
751
|
+
current_tables = _diffable(current['tables'])
|
|
752
|
+
# ⚠️ 虚拟类**整份**交给 `_virtual_payload()`(内置抽象基类也要画成卡片),
|
|
753
|
+
# 由它内部再剔一次 `builtin` 去算差异(见那个函数)。
|
|
700
754
|
current_virtuals = current['virtuals']
|
|
701
755
|
# 🔴 **先把基线里的两份分开**:`gitinfo.baseline_tables()` 现在回报的是**表 + 虚拟类**
|
|
702
756
|
# (见 `shape_ast.derive()`),而表那一套差异函数(`_diff` / `_field_diff` /
|
|
@@ -720,6 +774,7 @@ def payload() -> dict[str, Any]:
|
|
|
720
774
|
relation_diff = _relation_diff(baseline_tables, current_tables)
|
|
721
775
|
field_diff = _field_diff(baseline_tables, current_tables)
|
|
722
776
|
return {
|
|
777
|
+
# 🔴 这一份是**画在页面上**的(含 Django 自带的卡片);差异那几份都已经剔掉 `builtin`。
|
|
723
778
|
'tables': current['tables'],
|
|
724
779
|
'deletedTables': _deleted_tables(baseline_tables, diff['deleted']),
|
|
725
780
|
'diff': diff,
|
|
@@ -794,15 +849,22 @@ def _virtual_payload(
|
|
|
794
849
|
是 `[]`,而 `[]` 的语义是"基线里确实一个虚拟类都没有" ⇒ 现在有的都算新增。
|
|
795
850
|
**这是刻意的**:虚拟类是这一版才有的概念,第一个用它的项目本来就该在画布上
|
|
796
851
|
看到"新增"(那正是"这个提交开始有了泛化"这句话)。
|
|
852
|
+
🔴 **内置卡片(`builtin`)不参与**:卡片照发(`virtuals` 原样),差异只算剔掉它们的那一份
|
|
853
|
+
—— Django 自带的那几个抽象基类在任何基线里都不存在,比进去就是一条假"新增"。
|
|
797
854
|
🔴 **子表(`subclasses`)挂到卡片上**:虚拟卡片要写得出"哪几张表在继承我" ——
|
|
798
855
|
没有它,虚拟类被改名 / 被删时人不知道会波及谁,而那是真要回去改代码的事。
|
|
799
856
|
判据只有一处:`table.virtualParents`(`_card()` 算的)里有没有这个名字。
|
|
800
857
|
"""
|
|
801
|
-
|
|
802
|
-
|
|
858
|
+
diffable = _diffable(virtuals)
|
|
859
|
+
virtual_diff = _diff(baseline_virtuals, diffable)
|
|
860
|
+
virtual_field_diff = _field_diff(baseline_virtuals, diffable)
|
|
803
861
|
# ⚠️ 先算差异再往卡片上挂 `subclasses`:`_diff` / `_field_diff` 比的是**结构**
|
|
804
862
|
# (名字 + 列的形状),多挂一个键不影响它们 —— 但先算更不容易看错。
|
|
805
863
|
for item in virtuals:
|
|
864
|
+
# 🔴 内置卡片(Django 自带的抽象基类)不挂 `subclasses`:那会把"Django 的那一层"
|
|
865
|
+
# 画成这个项目的泛化关系(要的是**只展示、不连继承线**,见 `_builtin_virtuals()`)。
|
|
866
|
+
if item.get('builtin'):
|
|
867
|
+
continue
|
|
806
868
|
name = str(item.get('name') or '')
|
|
807
869
|
item['subclasses'] = sorted(
|
|
808
870
|
str(table.get('name'))
|
|
@@ -608,8 +608,9 @@ def _read_app_sources(
|
|
|
608
608
|
读工作区那份等于**把平台自己未提交的改动吸收进基线**,两边一模一样 ⇒ 差异恒为空。
|
|
609
609
|
② 否则读**装着的这一份**(工作区文件):它不是仓库(`pip install` 出来的 wheel、
|
|
610
610
|
`.zip` 里、源码不在本地),或者调用方要的本来就是"现在这一份"(`current_sources()`)。
|
|
611
|
-
🔴 **`django.contrib.*`
|
|
612
|
-
|
|
611
|
+
🔴 **`django.contrib.*` 一律不读**:它们是 Django 自己的表(画布上归 `Django` 模块、
|
|
612
|
+
**不参与 diff**,见 `data._diffable()`),读进来只会以相反的符号冒出来(基线有、当前没有
|
|
613
|
+
⇒ 报一堆"删除")。
|
|
613
614
|
:param packages: 只读这几个包(`None` = 全部)。`current_sources()` 用它把"装着的 app"
|
|
614
615
|
收窄成"**模型真的定义在**这个 app 里"的那些 —— `INSTALLED_APPS` 里还住着
|
|
615
616
|
`apps.todos` 这种没有 `models.py` 的条目,收窄了才与 `data.shape()` 同形。
|
|
@@ -127,7 +127,8 @@ BIG_PK_TYPE = 'bigint'
|
|
|
127
127
|
#:
|
|
128
128
|
#: 🔴 为什么需要它:`INSTALLED_APPS` 里的 `django.contrib.admin` **也带模型**
|
|
129
129
|
#: (`LogEntry` 有一条指向用户表的外键),于是 `_meta` 会给出 `labull_user.logentry`
|
|
130
|
-
#: 这条反向关联 —— 而它的来源 `django_admin_log`
|
|
130
|
+
#: 这条反向关联 —— 而它的来源 `django_admin_log` 是 Django 自己的表:3.0.0 起画布**画**它
|
|
131
|
+
#: (`Django` 模块),但**不参与 diff**,所以基线这一侧仍然一条都不读它。
|
|
131
132
|
#: 不排掉这一档,"这个项目里从没人声明过的那条关联"会一直显示成"新增"(本仓库就是这样)。
|
|
132
133
|
#: ⚠️ 判据是**包名**(`django.contrib.*` 全是 Django 自己的),不是"有没有源码":
|
|
133
134
|
#: 本项目里的 app 拿不到源码是**配置问题**,那种情况该报出来,不该被这里悄悄吞掉。
|
|
@@ -1020,7 +1021,7 @@ class _Project:
|
|
|
1020
1021
|
`AppConfig.label` 改成别的 —— 两种都算命中(宁可多画一张表,也不要因为一个名字
|
|
1021
1022
|
没对上就把整张表报成"被删了")。
|
|
1022
1023
|
🔴 **Django 自己的 app 一律不算"这个项目的 app"**(见 `DJANGO_APPS_PREFIX`):
|
|
1023
|
-
|
|
1024
|
+
它们的确装在里面,但它们的模型**不参与 diff**(画布上归 `Django` 模块,只展示),
|
|
1024
1025
|
而它们的模型会往**你的**表上挂反向关联(`labull_user.logentry`)——
|
|
1025
1026
|
那一条在你的源码里根本不存在,算进来就是一条假差异。
|
|
1026
1027
|
🔴 **要认"包路径"与"标签"两种写法**:这里的入参是 `model.app`(包路径,
|
|
@@ -158,7 +158,7 @@ ENV_ITEMS: tuple[EnvItem, ...] = (
|
|
|
158
158
|
# ── 平台开关 ──
|
|
159
159
|
EnvItem(
|
|
160
160
|
name='BOOTSTRAP_ADMIN_WORK_IDS',
|
|
161
|
-
brief='
|
|
161
|
+
brief='管理员名单:启动时把这些人加进「平台管理员」权限包(`edit_magic_block` 所在的那个组),名单之外的一律摘掉(逗号分隔,上限 200;没配就是没有管理员)',
|
|
162
162
|
hint='BOOTSTRAP_ADMIN_WORK_IDS=1001',
|
|
163
163
|
group='平台开关',
|
|
164
164
|
),
|