labull-framework 0.1.0__tar.gz → 0.1.2__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 (48) hide show
  1. {labull_framework-0.1.0/labull_framework.egg-info → labull_framework-0.1.2}/PKG-INFO +3 -3
  2. {labull_framework-0.1.0 → labull_framework-0.1.2}/README-pypi.md +112 -112
  3. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/admin.py +1 -1
  4. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/apps.py +33 -33
  5. labull_framework-0.1.2/labull_framework/auth/bootstrap.py +75 -0
  6. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/canvas/data.py +228 -1
  7. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/canvas/static/canvas.css +17 -2
  8. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/canvas/static/canvas.js +411 -82
  9. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/env.py +203 -187
  10. labull_framework-0.1.2/labull_framework/magic_block/deepcheck.py +764 -0
  11. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/magic_block/devkit.py +451 -428
  12. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/magic_block/health.py +222 -220
  13. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/magic_block/sdk.py +265 -245
  14. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/magic_block/service.py +421 -405
  15. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/magic_block/validate.py +590 -558
  16. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/magic_block/views.py +409 -407
  17. {labull_framework-0.1.0 → labull_framework-0.1.2/labull_framework.egg-info}/PKG-INFO +3 -3
  18. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework.egg-info/SOURCES.txt +1 -0
  19. {labull_framework-0.1.0 → labull_framework-0.1.2}/pyproject.toml +1 -1
  20. labull_framework-0.1.0/labull_framework/auth/bootstrap.py +0 -47
  21. {labull_framework-0.1.0 → labull_framework-0.1.2}/LICENSE +0 -0
  22. {labull_framework-0.1.0 → labull_framework-0.1.2}/MANIFEST.in +0 -0
  23. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/__init__.py +0 -0
  24. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/auth/__init__.py +0 -0
  25. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/auth/backend.py +0 -0
  26. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/auth/sso.py +0 -0
  27. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/auth/views.py +0 -0
  28. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/canvas/__init__.py +0 -0
  29. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/canvas/gitinfo.py +0 -0
  30. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/canvas/static/page.html +0 -0
  31. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/canvas/views.py +0 -0
  32. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/jsonio.py +0 -0
  33. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/magic_block/__init__.py +0 -0
  34. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/magic_block/dispatch.py +0 -0
  35. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/magic_block/export.py +0 -0
  36. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/magic_block/registry.py +0 -0
  37. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/management/__init__.py +0 -0
  38. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/management/commands/__init__.py +0 -0
  39. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/management/commands/labull_contract.py +0 -0
  40. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/migrations/0001_initial.py +0 -0
  41. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/migrations/__init__.py +0 -0
  42. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/models.py +0 -0
  43. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/settings.py +0 -0
  44. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework/urls.py +0 -0
  45. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework.egg-info/dependency_links.txt +0 -0
  46. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework.egg-info/requires.txt +0 -0
  47. {labull_framework-0.1.0 → labull_framework-0.1.2}/labull_framework.egg-info/top_level.txt +0 -0
  48. {labull_framework-0.1.0 → labull_framework-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 0.1.0
3
+ Version: 0.1.2
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
@@ -63,7 +63,7 @@ from labull_framework.urls import urlpatterns as labull_urls
63
63
  urlpatterns = [*labull_urls, path('admin/', admin.site.urls)]
64
64
  ```
65
65
 
66
- `wsgi.py` 里一行(**别漏**,补管理员与启动体检都在这里):
66
+ `wsgi.py` 里一行(**别漏**,重设管理员名单与启动体检都在这里):
67
67
 
68
68
  ```python
69
69
  from labull_framework.magic_block.health import startup_once
@@ -93,7 +93,7 @@ CORS_ORIGIN=http://localhost:5173
93
93
 
94
94
  | 想要什么 | 怎么做 |
95
95
  |---|---|
96
- | **谁是管理员** | Django 的 `is_staff`(admin 里勾,或配 `BOOTSTRAP_ADMIN_WORK_IDS` 写工号) |
96
+ | **谁是管理员** | 配 `BOOTSTRAP_ADMIN_WORK_IDS=工号,工号` —— 它是**声明式**的名单:启动时名单里的人补上 `is_staff`,**名单之外的一律清掉**(没配就是"一个管理员都没有")。它不碰 `is_superuser`,所以别拿超管当后门 |
97
97
  | **看数据模型那一页** | 设 `LABULL_SCHEMA_CANVAS=on`,浏览器打开 `/` |
98
98
  | **导出一份模型形状** | `python manage.py labull_contract --out .labull/contract.json` |
99
99
 
@@ -1,112 +1,112 @@
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
- | **谁是管理员** | Django 的 `is_staff`(admin 里勾,或配 `BOOTSTRAP_ADMIN_WORK_IDS` 写工号) |
69
- | **看数据模型那一页** | 设 `LABULL_SCHEMA_CANVAS=on`,浏览器打开 `/` |
70
- | **导出一份模型形状** | `python manage.py labull_contract --out .labull/contract.json` |
71
-
72
- ## magic block
73
-
74
- 一个块 = **一个 uuid + 一整个文件包**:
75
-
76
- ```
77
- 我的块/
78
- ├─ index.open.html 入口(必备)
79
- ├─ magic-block.open.yaml 清单(必备):id / module / name / brief 四个键
80
- ├─ 任意 js / css
81
- └─ backend_open.py 块自己的后端(可选)
82
- ```
83
-
84
- 块后端与项目里别的 Python 代码没有区别:
85
-
86
- ```python
87
- from labull_framework.magic_block.sdk import MagicBackendAPI, errors
88
-
89
- class Order:
90
- @MagicBackendAPI() # 省略 permission = 任何登录用户都能调
91
- def list(self, ctx, params):
92
- from modules.orders.models import Order
93
- return [{'id': o.id} for o in Order.objects.all()[:20]]
94
-
95
- @MagicBackendAPI(permission='labull_framework.edit_blocks')
96
- def close(self, ctx, params):
97
- if not params.get('id'):
98
- raise errors.BadRequest('要给我一个 id')
99
- return {'ok': True}
100
- ```
101
-
102
- 配置:环境变量里**带 `BLOCK_` 前缀**的才会进 `ctx.config`(`BLOCK_OCR_KEY=a` ⇒ `ctx.config['OCR_KEY']`)。
103
-
104
- ## 运行环境
105
-
106
- - Python ≥ 3.10
107
- - Django ≥ 4.2(在 6.1 上验证过)
108
- - PyJWT(带 `crypto` 后端)
109
-
110
- ## License
111
-
112
- 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_staff`,**名单之外的一律清掉**(没配就是"一个管理员都没有")。它不碰 `is_superuser`,所以别拿超管当后门 |
69
+ | **看数据模型那一页** | 设 `LABULL_SCHEMA_CANVAS=on`,浏览器打开 `/` |
70
+ | **导出一份模型形状** | `python manage.py labull_contract --out .labull/contract.json` |
71
+
72
+ ## magic block
73
+
74
+ 一个块 = **一个 uuid + 一整个文件包**:
75
+
76
+ ```
77
+ 我的块/
78
+ ├─ index.open.html 入口(必备)
79
+ ├─ magic-block.open.yaml 清单(必备):id / module / name / brief 四个键
80
+ ├─ 任意 js / css
81
+ └─ backend_open.py 块自己的后端(可选)
82
+ ```
83
+
84
+ 块后端与项目里别的 Python 代码没有区别:
85
+
86
+ ```python
87
+ from labull_framework.magic_block.sdk import MagicBackendAPI, errors
88
+
89
+ class Order:
90
+ @MagicBackendAPI() # 省略 permission = 任何登录用户都能调
91
+ def list(self, ctx, params):
92
+ from modules.orders.models import Order
93
+ return [{'id': o.id} for o in Order.objects.all()[:20]]
94
+
95
+ @MagicBackendAPI(permission='labull_framework.edit_blocks')
96
+ def close(self, ctx, params):
97
+ if not params.get('id'):
98
+ raise errors.BadRequest('要给我一个 id')
99
+ return {'ok': True}
100
+ ```
101
+
102
+ 配置:环境变量里**带 `BLOCK_` 前缀**的才会进 `ctx.config`(`BLOCK_OCR_KEY=a` ⇒ `ctx.config['OCR_KEY']`)。
103
+
104
+ ## 运行环境
105
+
106
+ - Python ≥ 3.10
107
+ - Django ≥ 4.2(在 6.1 上验证过)
108
+ - PyJWT(带 `crypto` 后端)
109
+
110
+ ## License
111
+
112
+ MIT
@@ -59,5 +59,5 @@ class MagicBlockAdmin(admin.ModelAdmin):
59
59
  return None
60
60
 
61
61
  def has_add_permission(self, request) -> bool:
62
- # 块的 uuid 由开发者在前端写死(`<MagicBlockHost uuid="…">`),admin 里"新增"没有意义。
62
+ # 块的 uuid 由开发者在前端路由表里写死(`MagicBlockPage` 的 `props.uuid`),admin 里"新增"没有意义。
63
63
  return False
@@ -1,33 +1,33 @@
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
+
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()
@@ -0,0 +1,75 @@
1
+ """启动时按 `BOOTSTRAP_ADMIN_WORK_IDS` **重设**谁是管理员 —— **这是"管理员名单"的唯一入口**。
2
+
3
+ 🔴 判据是**声明式**的:那份清单**就是**管理员名单。每次启动都拿它和数据库对一遍 ——
4
+ 名单里的人补上 `is_staff=True`,**名单之外的(以及没有工号的)一律清成 `False`**。
5
+ 于是"某人离职 / 不该再进 admin"这件事只需要从 `.env` 里删掉一个工号 + 重启,不必记得去 admin 里点。
6
+ 🔴 **没配 / 是空 ⇒ 把所有人的 `is_staff` 清掉**("没配"的语义就是"管理员一个都没有")。
7
+ ⚠️ 这是刻意选的**fail-closed**:`.env` 少写一行、部署时忘了带这个变量,后果是**没有人能进 /admin**
8
+ —— 而不是"上一个被删掉的人悄悄留着权限"。要救回来:把那一行补回去重启,
9
+ 或者 `python manage.py shell` 里手动 `User.objects.filter(work_id='1001').update(is_staff=True)`。
10
+ ⚠️ 反过来说:**它不碰 `is_superuser`**(那是 Django 的另一层,拥有全部权限,不归这份名单管)。
11
+ 🔴 清单超过 `MAX_WORK_IDS` 时**只取前 200 个**(防误粘)—— 于是第 201 个之后的人会被停掉,
12
+ 所以那一条 warning 必须显式说出来(不然症状是"名单明明写了,他就是进不去")。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+
19
+ from ..env import read_env
20
+ from ..models import User
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ MAX_WORK_IDS = 200
25
+
26
+
27
+ def sync_staff_work_ids() -> int:
28
+ """把 `is_staff` 重设成"只有名单里的人有",回这次**真的**改了几个人(上岗 + 下岗)。
29
+
30
+ 🔴 **永不抛**:这是启动路径,库连不上 / 表还没迁移都只该在日志里留一句 ——
31
+ 重设管理员失败没有理由让整个后端起不来。
32
+ 🔴 `update()` 的返回值是**匹配到的行数**,不是"真的改过的行数"(Django 不跳过"值本来就一样"的行)。
33
+ 所以"这次动了几个人"必须先查一遍现状再算差集 —— 拿返回值当"改动数"的话,
34
+ 同一条名单每启动一次都会报"改了 N 个"(真踩过:第二次仍然说改了 2 个),
35
+ 而这个数字正是运维判断"这次启动有没有动过权限"的唯一线索。
36
+ 🔴 判据是**名单**,不是"库里现在谁有 staff":`work_id` 为 `NULL` 的行永远不在名单里
37
+ (SQL 的 `NULL IN (…)` / `NULL NOT IN (…)` 都是 unknown),所以"没有工号的人"会被清掉 ——
38
+ 这是对的(认人只认工号),但也意味着**没有工号的人不可能当管理员**。
39
+ """
40
+ raw = read_env('BOOTSTRAP_ADMIN_WORK_IDS')
41
+ work_ids = [item.strip() for item in raw.split(',') if item.strip()]
42
+ if len(work_ids) > MAX_WORK_IDS:
43
+ logger.warning(
44
+ 'BOOTSTRAP_ADMIN_WORK_IDS 有 %s 个工号,本次只认前 %s 个(上限 %s):'
45
+ '**多出来的那些人会被停用**(名单就是全部管理员)—— 请拆成多批,或确认这一行是不是误粘了。',
46
+ len(work_ids), MAX_WORK_IDS, MAX_WORK_IDS,
47
+ )
48
+ work_ids = work_ids[:MAX_WORK_IDS]
49
+ if not work_ids:
50
+ logger.warning(
51
+ 'BOOTSTRAP_ADMIN_WORK_IDS 没配 / 是空的:按"管理员一个都没有"处理,'
52
+ '会把所有人的 is_staff 清成 False(于是谁都进不了 /admin)。'
53
+ '要指定管理员就在 .env 里写一行 BOOTSTRAP_ADMIN_WORK_IDS=<工号>[,<工号>…]。'
54
+ )
55
+
56
+ try:
57
+ wanted = list(User.objects.filter(work_id__in=work_ids).values_list('pk', flat=True))
58
+ unwanted = list(
59
+ User.objects.filter(is_staff=True).exclude(work_id__in=work_ids).values_list('pk', flat=True)
60
+ )
61
+ # 🔴 `update()` 而不是逐个 `save()`:一条 SQL 就完事,也不碰 `date_joined` / `last_login`
62
+ # 这类自动字段("改角色"不该让"这个人什么时候注册的"跟着变)。
63
+ # 🔴 两个 `.update()` 的 WHERE 里都要带 `is_staff` 那一半:不带的话每次启动都会真写一遍
64
+ # (幂等没了、行锁白拿)。
65
+ added = User.objects.filter(pk__in=wanted).filter(is_staff=False).update(is_staff=True) if wanted else 0
66
+ removed = User.objects.filter(pk__in=unwanted).update(is_staff=False) if unwanted else 0
67
+ except Exception as error: # noqa: BLE001 —— 见 docstring:启动路径永不抛
68
+ logger.error('按 BOOTSTRAP_ADMIN_WORK_IDS 重设 is_staff 失败(不影响启动):%s', error)
69
+ return 0
70
+ if added or removed:
71
+ logger.info(
72
+ '按 BOOTSTRAP_ADMIN_WORK_IDS 重设管理员:设上 %s 个、清掉 %s 个(名单里有 %s 个工号)',
73
+ added, removed, len(work_ids),
74
+ )
75
+ return added + removed