labull-framework 3.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.
Files changed (68) hide show
  1. {labull_framework-3.0.0/labull_framework.egg-info → labull_framework-3.0.1}/PKG-INFO +1 -1
  2. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/admin.py +1 -1
  3. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/auth/bootstrap.py +7 -7
  4. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/auth/views.py +4 -7
  5. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/flow.py +12 -8
  6. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/devkit.py +1 -1
  7. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/dispatch.py +1 -4
  8. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/sdk.py +2 -5
  9. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/views.py +22 -20
  10. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/models.py +3 -2
  11. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/permissions.py +19 -34
  12. {labull_framework-3.0.0 → labull_framework-3.0.1/labull_framework.egg-info}/PKG-INFO +1 -1
  13. {labull_framework-3.0.0 → labull_framework-3.0.1}/pyproject.toml +1 -1
  14. {labull_framework-3.0.0 → labull_framework-3.0.1}/LICENSE +0 -0
  15. {labull_framework-3.0.0 → labull_framework-3.0.1}/MANIFEST.in +0 -0
  16. {labull_framework-3.0.0 → labull_framework-3.0.1}/README-pypi.md +0 -0
  17. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/__init__.py +0 -0
  18. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/apps.py +0 -0
  19. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/auth/__init__.py +0 -0
  20. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/auth/backend.py +0 -0
  21. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/auth/sso.py +0 -0
  22. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/canvas/__init__.py +0 -0
  23. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/canvas/data.py +0 -0
  24. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/canvas/gitinfo.py +0 -0
  25. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/canvas/shape_ast.py +0 -0
  26. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/canvas/static/canvas.css +0 -0
  27. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/canvas/static/canvas.js +0 -0
  28. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/canvas/static/page.html +0 -0
  29. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/canvas/views.py +0 -0
  30. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/env.py +0 -0
  31. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/jsonio.py +0 -0
  32. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/__init__.py +0 -0
  33. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/blockcheck.py +0 -0
  34. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/blockpackage.py +0 -0
  35. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/blocktypes.py +0 -0
  36. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/deepcheck.py +0 -0
  37. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/export.py +0 -0
  38. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/health.py +0 -0
  39. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/manifest.py +0 -0
  40. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/parallel.py +0 -0
  41. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/registry.py +0 -0
  42. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/requires.py +0 -0
  43. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/service.py +0 -0
  44. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/subproc.py +0 -0
  45. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/types/__init__.py +0 -0
  46. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/types/plain.py +0 -0
  47. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/types/workflow.py +0 -0
  48. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/magic_block/validate.py +0 -0
  49. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/management/__init__.py +0 -0
  50. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/management/commands/__init__.py +0 -0
  51. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/management/commands/labull_blocktype.py +0 -0
  52. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/management/commands/labull_contract.py +0 -0
  53. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/management/commands/labull_requires.py +0 -0
  54. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0001_initial.py +0 -0
  55. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0002_remove_magicblock_prev_files_and_more.py +0 -0
  56. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0003_magicblock_tags.py +0 -0
  57. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0004_magicblock_detail.py +0 -0
  58. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0005_labullrole.py +0 -0
  59. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/migrations/0006_default_groups_permissions.py +0 -0
  60. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/migrations/__init__.py +0 -0
  61. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/settings.py +0 -0
  62. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/urls.py +0 -0
  63. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework/virtual.py +0 -0
  64. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework.egg-info/SOURCES.txt +0 -0
  65. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework.egg-info/dependency_links.txt +0 -0
  66. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework.egg-info/requires.txt +0 -0
  67. {labull_framework-3.0.0 → labull_framework-3.0.1}/labull_framework.egg-info/top_level.txt +0 -0
  68. {labull_framework-3.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.0.0
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
@@ -23,7 +23,7 @@ class UserAdmin(admin.ModelAdmin):
23
23
 
24
24
  ⚠️ `username` 装的是 Authing 的 `sub`(登录时同步进来的),所以它**只读** ——
25
25
  改它等于把这一行与那个身份切开。
26
- 🔴 **"是不是平台管理员" = 有没有 `labull_framework.edit_magic_block`**(`permissions.is_admin()`)——
26
+ 🔴 **"能不能编辑块" = 有没有 `labull_framework.edit_magic_block`**(`permissions.EDIT_PERMISSION`)——
27
27
  直接给在这个人身上、或者他所在的某个权限包里都算。这里不摆 `is_staff`:
28
28
  那一列只留给 Django 自己的 `/admin`,平台不读它。
29
29
  🔴 `groups` / `user_permissions` 两栏就是全部授权入口(组是权限包,权限码才是契约)。
@@ -4,10 +4,10 @@
4
4
  (`edit_magic_block` 所在的权限包)对一遍 —— 名单里的人加进那个组,
5
5
  **名单之外的(以及没有工号的)一律摘掉**。于是"某人离职 / 不该再当管理员"这件事只需要从 `.env`
6
6
  里删掉一个工号 + 重启,不必记得去点。
7
- 🔴 "是不是管理员"的唯一判据是 **`has_perm('labull_framework.edit_magic_block')`**
8
- (`permissions.is_admin()`)—— **不是**组名、**不是** Django 的 `is_staff`(那一列只留给
9
- Django 自己的 `/admin`)。这里之所以要找一个"组",只是因为**写**的时候要有个落点;
10
- 找法按**权限**找(`permissions.admin_group()`),所以项目把那个组改名也不影响这份名单。
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
11
  🔴 **没配 / 是空 ⇒ 把所有人的管理员权限摘掉**("没配"的语义就是"管理员一个都没有")。
12
12
  ⚠️ 这是刻意选的 **fail-closed**:`.env` 少写一行、部署时忘了带这个变量,后果是**一个管理员都没有**
13
13
  —— 而不是"上一个被删掉的人悄悄留着权限"。要救回来:把那一行补回去重启。
@@ -24,7 +24,7 @@ import logging
24
24
 
25
25
  from ..env import read_env
26
26
  from ..models import User
27
- from ..permissions import ADMIN_PERMISSION, admin_group
27
+ from ..permissions import EDIT_PERMISSION, group_holding
28
28
 
29
29
  logger = logging.getLogger(__name__)
30
30
 
@@ -71,11 +71,11 @@ def sync_admin_work_ids() -> int:
71
71
  'BOOTSTRAP_ADMIN_WORK_IDS=<工号>[,<工号>…]。'
72
72
  )
73
73
 
74
- group = admin_group()
74
+ group = group_holding(EDIT_PERMISSION)
75
75
  if group is None:
76
76
  # 迁移会建"平台管理员"那个权限包;走到这里说明库还没迁移(或权限行还没建出来),
77
77
  # 不在启动路径上制造第二次失败 —— 下一次启动/迁移之后它就有了。
78
- logger.error('还没有装 %s 的组(权限包):跳过管理员名单同步', ADMIN_PERMISSION)
78
+ logger.error('还没有装 %s 的组(权限包):跳过管理员名单同步', EDIT_PERMISSION)
79
79
  return 0
80
80
 
81
81
  try:
@@ -133,19 +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` = **有没有 `labull_framework.edit_magic_block`**(`permissions.is_admin()`)——
137
- 不是 Django 的 `is_staff`、也不是某个组名。
138
- 🔴 `groups` / `permissions` 是 Django 原生的那一套:**权限是契约**(`app_label.codename`),
139
- 组只是权限包(`groups` 只给人看/排查,前端不要拿组名做判断)。
136
+ 🔴 **没有 `isAdmin` 这类派生位**:能做什么只看 `permissions`(`app_label.codename`,
137
+ 直接给的 + 经组给的)。前端要判"能不能编辑块"就查
138
+ `permissions.includes('labull_framework.edit_magic_block')`。
139
+ 🔴 `groups` 只是权限包的名字(只给人看/排查),**不要**拿组名做判断。
140
140
  """
141
- from ..permissions import is_admin
142
-
143
141
  return {
144
142
  'id': user.pk,
145
143
  'username': user.get_username(),
146
144
  'name': user.name,
147
145
  'workId': user.work_id,
148
- 'isAdmin': is_admin(user),
149
146
  'isSuperuser': user.is_superuser,
150
147
  'isActive': user.is_active,
151
148
  'groups': [group.name for group in user.groups.all()],
@@ -24,9 +24,8 @@ from typing import Any, Callable, Iterable, Mapping
24
24
 
25
25
  from .magic_block.sdk import MagicBackendHTTPError
26
26
  from .permissions import (
27
- ADMIN_PERMISSION,
27
+ EDIT_PERMISSION,
28
28
  PERMISSION_PATTERN,
29
- is_admin,
30
29
  permissions_of,
31
30
  split_permission,
32
31
  )
@@ -615,8 +614,9 @@ class Flow:
615
614
 
616
615
  # ──────────────────────────── 规则串(语法与求值只有一处) ────────────────────────────
617
616
  #
618
- # 认人只认 **permission**(`app_label.codename`):`is_admin()` / `permissions_of()` /
619
- # `ADMIN_PERMISSION` 都在 `labull_framework/permissions.py`(那一份是"谁能做什么"的唯一真源)。
617
+ # 认人只认 **permission**(`app_label.codename`):`permissions_of()` / `EDIT_PERMISSION` /
618
+ # `split_permission()` 都在 `labull_framework/permissions.py`(那一份是"谁能做什么"的唯一真源)。
619
+ # 🔴 没有 `is_admin()` / `has_perm()` 那类包装:判断一律写成 `<权限码> in permissions_of(user)`。
620
620
 
621
621
 
622
622
  def _permission_table() -> set[str] | None:
@@ -757,7 +757,7 @@ class Candidates:
757
757
  def allows(self, user: Any) -> bool:
758
758
  """这个人能不能办(`ctx.user` 或 Django 的 User 实例都行)。"""
759
759
  if self.admin_only:
760
- return is_admin(user)
760
+ return EDIT_PERMISSION in permissions_of(user)
761
761
  if _user_id(user) in self.ids:
762
762
  return True
763
763
  return bool(permissions_of(user) & set(self.permissions))
@@ -932,10 +932,14 @@ def _visible_moves(flow: Flow, step: Step, row: Any) -> tuple[Move, ...]:
932
932
  def _is_admin_bypass(flow: Flow, user: Any) -> bool:
933
933
  """管理员旁路(`Flow.admin_bypass`):只放行**候选人闸门**,不放行 `confirm_permissions`。
934
934
 
935
- 🔴 "管理员" = `has_perm('labull_framework.edit_magic_block')`(`permissions.is_admin()`),
935
+ 🔴 "能旁路的人" = **有 `labull_framework.edit_magic_block` 权限的人**(`EDIT_PERMISSION`),
936
936
  不是 Django 的 `is_staff`、也不是某个组名。
937
937
  """
938
- return bool(flow.admin_bypass and user is not None and is_admin(user))
938
+ return bool(
939
+ flow.admin_bypass
940
+ and user is not None
941
+ and EDIT_PERMISSION in permissions_of(user)
942
+ )
939
943
 
940
944
 
941
945
  def _is_confirm_permission(flow: Flow, move: Move, user: Any) -> bool:
@@ -1057,7 +1061,7 @@ def _ids_of(found: Candidates) -> list[str]:
1057
1061
  users = get_user_model().objects.all()
1058
1062
  wanted = set(found.permissions)
1059
1063
  if found.admin_only:
1060
- wanted.add(ADMIN_PERMISSION)
1064
+ wanted.add(EDIT_PERMISSION)
1061
1065
  if wanted:
1062
1066
  query = Q()
1063
1067
  for permission in sorted(wanted):
@@ -1565,7 +1565,7 @@ MagicSteps.renderBar(document.getElementById('steps'), payload) // 步骤条
1565
1565
 
1566
1566
  ### 3. "权限"这个词在这个项目里指哪两类
1567
1567
 
1568
- 1. **平台级能力**:`ctx.user.is_admin`(平台管理员 = 有 `labull_framework.edit_magic_block`)、`ctx.user.permissions`(Django 的 `app_label.codename`,**这才是判据**);`ctx.user.groups` 只是权限包的名字(只给人看);后端路由可以声明 `@MagicBackendAPI(permission='app_label.codename')`。
1568
+ 1. **平台级能力**:`ctx.user.permissions`(Django 的 `app_label.codename` —— **判据只有它**;要问"能不能编辑魔法块"就是 `'labull_framework.edit_magic_block' in ctx.user.permissions`,没有 `is_admin` 这种派生位);`ctx.user.groups` 只是权限包的名字(只给人看);后端路由可以声明 `@MagicBackendAPI(permission='app_label.codename')`。
1569
1569
  2. **Django 模型级 CRUD**(`auth_permission` 里那几组 `add_/change_/delete_/view_`):**通常不是**需求说的那个"权限"。拿它当业务权限目录 = **越界**(见规矩 1)。
1570
1570
 
1571
1571
  ### 4. 开工前先读四样(**3 分钟,省几小时**)
@@ -134,15 +134,12 @@ def user_of(user: Any) -> BlockUser:
134
134
  🔴 匿名用户也映射得出来:块里的判据不用先判空。
135
135
  """
136
136
  if user is None or not getattr(user, 'is_authenticated', False):
137
- return BlockUser(id='', username='', work_id='', name='', is_admin=False)
138
- from ..permissions import is_admin
139
-
137
+ return BlockUser(id='', username='', work_id='', name='')
140
138
  return BlockUser(
141
139
  id=str(user.pk or ''),
142
140
  username=str(getattr(user, 'get_username', lambda: '')() or ''),
143
141
  work_id=str(getattr(user, 'work_id', '') or ''),
144
142
  name=str(getattr(user, 'name', '') or ''),
145
- is_admin=is_admin(user),
146
143
  groups=_names(user.groups.values_list('name', flat=True)),
147
144
  permissions=_names(sorted(user.get_all_permissions())),
148
145
  )
@@ -309,7 +309,8 @@ class BlockUser:
309
309
  🔴 `groups` / `permissions` 给的是**名字**(组名 / 权限 `app_label.codename`),不是 Django 对象:
310
310
  块里判权限用 `'app.codename' in ctx.user.permissions` 就够,不必碰 ORM。
311
311
  🔴 **权限才是契约,组只是权限包**:判"能不能做"永远看 `permissions`,**不要**看 `groups`
312
- 里的组名(组名只是标签,项目随时可能改名)。
312
+ 里的组名(组名只是标签,项目随时可能改名)。要问"这个人能不能编辑块"就是
313
+ `'labull_framework.edit_magic_block' in ctx.user.permissions` —— 没有 `is_admin` 这种派生位。
313
314
  """
314
315
 
315
316
  #: 本地用户主键(Django 的 `pk`)。
@@ -320,10 +321,6 @@ class BlockUser:
320
321
  work_id: str
321
322
  #: 显示名(拿不到就是空串 —— 不编造)。
322
323
  name: str
323
- #: **平台管理员** = `has_perm('labull_framework.edit_magic_block')`(直接给的 / 经组给的都算)。
324
- #: 🔴 它**不是** Django 的 `is_staff`(那一列只留给 Django 自己的 `/admin`),
325
- #: 也**不是**某个组名(组只是权限包)。
326
- is_admin: bool
327
324
  #: 所在组的名字(只给人看 / 排查;**不要**拿组名当判据)。
328
325
  groups: tuple[str, ...] = ()
329
326
  #: 直接权限 + 组权限的 codename(`app_label.codename` 形式)。
@@ -1,8 +1,9 @@
1
1
  """
2
2
  `/magic-blocks/*` 与 `/bx/*` 的全部视图(路由由 `urls.py` 按函数名指过来)。
3
3
 
4
- 🔴 权限用**平台管理员**(`permissions.is_admin(user)` = `has_perm('labull_framework.edit_magic_block')`;
5
- **不是** Django 的 `is_staff`、也不是某个组名):写口与管理员读口要管理员(**自己写 `admin_only`**:
4
+ 🔴 权限用**权限码**(`permissions.EDIT_PERMISSION` = `labull_framework.edit_magic_block`,
5
+ **写死在框架里**;**不是** Django 的 `is_staff`、也不是某个组名):写口与管理员读口都要求
6
+ `EDIT_PERMISSION in permissions_of(user)`(**自己写 `edit_only`**:
6
7
  `@staff_member_required` 对未登录用户会 302 到 admin 登录页,而这里要的是 401 + JSON);
7
8
  `status` 那条口已经没了:任何登录用户能读的**只有 `render` 一条**,而且**永远不许**改成回整行 / 回全量 `files`。
8
9
  🔴 所有回话一律走 `jsonio`(一处出口:编码政策与错误形状都在那里)。
@@ -33,24 +34,25 @@ from .service import (
33
34
  )
34
35
 
35
36
 
36
- def admin_only(view: Callable[..., HttpResponse]) -> Callable[..., HttpResponse]:
37
- """管理员守卫:没登录 ⇒ **401 + JSON**;登录了但不是管理员 ⇒ **403 + JSON**。
37
+ def edit_only(view: Callable[..., HttpResponse]) -> Callable[..., HttpResponse]:
38
+ """编辑守卫(抽屉 / 控制台那几口):没登录 ⇒ **401 + JSON**;登录了但没有编辑权限 ⇒ **403 + JSON**。
38
39
 
39
- 🔴 "管理员" = **有没有 `labull_framework.edit_magic_block`**(直接给的 / 经权限包给的都算)——
40
- 不是 Django 的 `is_staff`、也不是某个组名。
40
+ 🔴 判据就一句:**`labull_framework.edit_magic_block in permissions_of(user)`**
41
+ (直接给的 / 经权限包给的都算)—— 这个码**写死在框架里**(`permissions.EDIT_PERMISSION`),
42
+ 它就是"能不能编辑魔法块"那一档;不是 Django 的 `is_staff`、也不是某个组名。
41
43
  🔴 两条判据分开报:一个是"你是谁",一个是"你能不能" —— 合成一条会让前端不知道该去登录还是找管理员。
42
44
  🔴 **不返回 302**:这几条是接口,前端要看状态码分支(302 会被 `fetch` 当成功跟着跳,最后拿到一页 HTML)。
43
45
  """
44
46
  from ..jsonio import no_identity
45
- from ..permissions import is_admin
47
+ from ..permissions import EDIT_PERMISSION, permissions_of
46
48
 
47
49
  @wraps(view)
48
50
  def wrapper(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
49
51
  user = getattr(request, 'user', None)
50
52
  if user is None or not getattr(user, 'is_authenticated', False):
51
53
  return no_identity()
52
- if not is_admin(user):
53
- return problem('这条接口只给管理员(需要 labull_framework.edit_magic_block 权限)', 403)
54
+ if EDIT_PERMISSION not in permissions_of(user):
55
+ return problem('这条接口只给能编辑魔法块的人(需要 labull_framework.edit_magic_block 权限)', 403)
54
56
  return view(request, *args, **kwargs)
55
57
 
56
58
  return wrapper
@@ -164,7 +166,7 @@ def _row_view(row: Any) -> dict:
164
166
  # ──────────────────────────── 管理员读口 ────────────────────────────
165
167
 
166
168
 
167
- @admin_only
169
+ @edit_only
168
170
  def index_view(request: HttpRequest) -> HttpResponse:
169
171
  """`GET /magic-blocks/index` —— 控制台用的块索引(**不带文件正文**)。
170
172
 
@@ -186,7 +188,7 @@ def index_view(request: HttpRequest) -> HttpResponse:
186
188
  })
187
189
 
188
190
 
189
- @admin_only
191
+ @edit_only
190
192
  def list_view(request: HttpRequest) -> HttpResponse:
191
193
  """`/magic-blocks` —— **GET 列全部块**(带正文,管理员控制台用);**POST 保存一个块**。
192
194
 
@@ -224,7 +226,7 @@ def list_view(request: HttpRequest) -> HttpResponse:
224
226
  return _json([_row_view(row) for row in rows])
225
227
 
226
228
 
227
- @admin_only
229
+ @edit_only
228
230
  def tags_view(request: HttpRequest, block_id: str) -> HttpResponse:
229
231
  """`/magic-blocks/<id>/tags` —— **只改标签**(`PUT`/`POST`),别的一律不碰。
230
232
 
@@ -248,7 +250,7 @@ def tags_view(request: HttpRequest, block_id: str) -> HttpResponse:
248
250
  return _json(_row_view(row))
249
251
 
250
252
 
251
- @admin_only
253
+ @edit_only
252
254
  def detail_field_view(request: HttpRequest, block_id: str) -> HttpResponse:
253
255
  """`/magic-blocks/<id>/detail` —— **只改详情介绍**(`PUT`/`POST`),别的一律不碰。
254
256
 
@@ -275,7 +277,7 @@ def detail_field_view(request: HttpRequest, block_id: str) -> HttpResponse:
275
277
  return _json(_row_view(row))
276
278
 
277
279
 
278
- @admin_only
280
+ @edit_only
279
281
  def versions_view(request: HttpRequest, block_id: str, number: str | None = None) -> HttpResponse:
280
282
  """`/magic-blocks/<id>/versions[/<number>]` —— 版本史的读、切、删、下。
281
283
 
@@ -372,7 +374,7 @@ def _zip_response(block_id: str, number: int, files: dict[str, str]) -> HttpResp
372
374
  return response
373
375
 
374
376
 
375
- @admin_only
377
+ @edit_only
376
378
  def detail_view(request: HttpRequest, block_id: str) -> HttpResponse:
377
379
  """`GET /magic-blocks/<id>` —— 一行(没有就是 `null`,不是 404)。
378
380
 
@@ -433,7 +435,7 @@ def render_view(request: HttpRequest) -> HttpResponse:
433
435
  # ──────────────────────────── 写口 ────────────────────────────
434
436
 
435
437
 
436
- @admin_only
438
+ @edit_only
437
439
  def rules_view(request: HttpRequest) -> HttpResponse:
438
440
  """`GET /magic-blocks/rules` —— **读回改动时要照的扫描规则**(认哪些扩展名 / 跳过哪些目录 / 几层到顶)。
439
441
 
@@ -450,7 +452,7 @@ def rules_view(request: HttpRequest) -> HttpResponse:
450
452
  return _json({'rules': scan_rules()})
451
453
 
452
454
 
453
- @admin_only
455
+ @edit_only
454
456
  def export_view(request: HttpRequest) -> HttpResponse:
455
457
  """`POST /magic-blocks/export` —— **magic block export**:把库里的块算成**一整个开发包**。
456
458
 
@@ -492,7 +494,7 @@ def export_view(request: HttpRequest) -> HttpResponse:
492
494
  return _json(export_package(blocks, framework_only=body.get('framework') is True))
493
495
 
494
496
 
495
- @admin_only
497
+ @edit_only
496
498
  def import_view(request: HttpRequest) -> HttpResponse:
497
499
  """`POST /magic-blocks/import` —— 把改好的目录**读回库**(一个块失败只报它自己)。
498
500
 
@@ -518,7 +520,7 @@ def import_view(request: HttpRequest) -> HttpResponse:
518
520
  return _json(write_dir(items, actor=str(body.get('updatedBy') or '')))
519
521
 
520
522
 
521
- @admin_only
523
+ @edit_only
522
524
  def read_view(request: HttpRequest) -> HttpResponse:
523
525
  """`POST /magic-blocks/read` —— 读一个目录,与库里的块逐块对比(**只读,不写库**)。
524
526
 
@@ -634,7 +636,7 @@ def _read_stream(body: dict) -> HttpResponse:
634
636
  return response
635
637
 
636
638
 
637
- @admin_only
639
+ @edit_only
638
640
  def check_view(request: HttpRequest) -> HttpResponse:
639
641
  """`POST /magic-blocks/check` —— 重新体检(默认全量,也可以点名几块),然后**统一写库**。
640
642
 
@@ -15,10 +15,11 @@ class User(AbstractUser):
15
15
  `AbstractUser` 自带 `username` 的 unique 约束,所以"同一身份两行"在库这一层就不可能发生。
16
16
  ⚠️ 没有本地口令:登录只走 OIDC,口令列由 `set_unusable_password()` 堵死。
17
17
 
18
- 🔴 **"能做什么"只看 `Permission`**(`app_label.codename`):`has_perm(...)` / `get_all_permissions()`。
18
+ 🔴 **"能做什么"只看 `Permission`**(`app_label.codename`):`permissions_of(user)` 是唯一读法,
19
+ 判断写成 `'app.codename' in permissions_of(user)`(**没有** `is_admin()` / `has_perm()` 这类包装)。
19
20
  授权只走 Django 原生的 `groups` / `user_permissions`(它们继承自 `PermissionsMixin`),
20
21
  `Group` 只是把一组权限**打包**(组名是标签,代码不按组名判断任何事)。
21
- "是不是平台管理员" = 有没有 `labull_framework.edit_magic_block`(见 `permissions.is_admin()`)——
22
+ "能不能编辑块" = 有没有 `labull_framework.edit_magic_block`(`permissions.EDIT_PERMISSION`,写死在框架里)——
22
23
  平台**不读也不写** Django 的 `is_staff`(那一列只留给 Django 自己的 `/admin`)。
23
24
  🔴 新用户**不自动进任何组**:那是项目的数据/策略(框架不装 `post_save`)。
24
25
  """
@@ -1,13 +1,17 @@
1
1
  """平台权限的唯一真源:**`Permission`(`app_label.codename`)** —— 组只是权限包。
2
2
 
3
3
  🔴 3.0.0 起**没有**"项目角色表"了(`LabullRole` / `User.roles` / `roles.py` 全删):
4
- 能做什么只看 `user.has_perm('app_label.codename')`(直接给的 + 经组给的,Django 已经算好了)。
5
- `Group` 只把一组权限**打包**(方便加人);**组名是标签**,代码里不许按组名判断身份 ——
6
- "是不是管理员"也一样(判据是 `edit_magic_block` 这个权限,见 `is_admin()`)。
4
+ 能做什么只看**这个人有哪些权限**(`permissions_of()`,直接给的 + 经组给的,Django 已经算好了)。
5
+ `Group` 只把一组权限**打包**(方便加人);**组名是标签**,代码里不许按组名判断身份。
6
+ 🔴 **没有 `is_admin()` / `has_perm()` 这类"判断方法"**:框架里所有授权判断都写成
7
+ `<权限码> in permissions_of(user)` —— 一条判据、一个写法。你要问"这个人能不能编辑块",
8
+ 就是问"他有没有 `labull_framework.edit_magic_block`"。
9
+ 🔴 **`edit_magic_block` 这个码写死在框架里**(`EDIT_PERMISSION`):它就是"能不能打开抽屉 /
10
+ 控制台去编辑魔法块"的那一档(写口、管理台、`Flow.admin_bypass`、示例块的守卫都按它判)。
11
+ 它是平台自己的契约,不改名、不配环境变量。
7
12
  🔴 框架**不装任何 `post_save` / `user_logged_in` 来给新用户加组**:新用户默认进哪个组是
8
13
  项目的数据/策略(示例项目给了一份可复制的实现:`backend/example/canvasdemo/signals.py`)。
9
- 这个模块只回答"这个人现在能做什么"。
10
- 🔴 匿名 / 没有会话(`user is None`)一律 False:没有"没登录所以放行"这回事。
14
+ 这个模块只回答"这个人现在有哪些权限"。
11
15
  """
12
16
 
13
17
  from __future__ import annotations
@@ -16,16 +20,12 @@ import re
16
20
  from typing import Any
17
21
 
18
22
  #: 平台自己的三个权限(与 `MagicBlock.Meta.permissions` **逐字同源**:那边是声明,这里是引用)。
19
- #: 🔴 只在这里写一次:`admin_only` / 启动名单 / 示例块的守卫全都引用这几个常量。
20
- ADMIN_PERMISSION = 'labull_framework.edit_magic_block'
23
+ #: 🔴 只在这里写一次:`admin_only` 那个守卫 / 启动名单 / 示例块的守卫全都引用这几个常量。
24
+ #: 🔴 `EDIT_PERMISSION` 是**写死**的:它就是"能不能编辑魔法块(抽屉 / 控制台)"那一档。
25
+ EDIT_PERMISSION = 'labull_framework.edit_magic_block'
21
26
  MANAGE_USERS_PERMISSION = 'labull_framework.manage_users'
22
27
  VIEW_PERMISSION = 'labull_framework.view_magicblock'
23
28
 
24
- #: 框架迁移建的两个默认组 —— **只是标签**(代码拿它建组/写进迁移,判断永远走 permission)。
25
- #: 🔴 项目想改名就改:行为一个字都不变(没有任何判据读这两个名字)。
26
- PLATFORM_ADMIN_GROUP = '平台管理员'
27
- DEFAULT_GROUP = '普通用户'
28
-
29
29
  #: `app_label.codename` 的形状(`flow.py` 里 `permission:` 的码也用它 —— 一份判据两处用)。
30
30
  PERMISSION_PATTERN = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$')
31
31
 
@@ -43,10 +43,12 @@ def split_permission(text: Any) -> tuple[str, str]:
43
43
 
44
44
 
45
45
  def permissions_of(user: Any) -> set[str]:
46
- """这个人**有效**的权限(`app_label.codename`:直接给的 ∪ 经组给的)。
46
+ """这个人**有效**的权限(`app_label.codename`:直接给的 ∪ 经组给的)—— 授权的唯一读法。
47
47
 
48
48
  🔴 两种"人"都认:Django 的 `User`(`get_all_permissions()`)与块里的 `ctx.user`
49
49
  (`BlockUser.permissions` 是现成的元组)—— 两处各写一份判据迟早会漂。
50
+ 🔴 判断一律写成 `'app.codename' in permissions_of(user)`(没有 `is_admin()` / `has_perm()`
51
+ 那种包装:多一层包装就多一处"两套解释")。
50
52
  """
51
53
  if user is None:
52
54
  return set()
@@ -59,31 +61,14 @@ def permissions_of(user: Any) -> set[str]:
59
61
  return {str(item) for item in (getattr(user, 'permissions', None) or ())}
60
62
 
61
63
 
62
- def has_perm(user: Any, permission: str) -> bool:
63
- """这个人有没有这个权限(`permission` 是 `app_label.codename`)。"""
64
- return str(permission or '') in permissions_of(user)
65
-
66
-
67
- def is_admin(user: Any) -> bool:
68
- """"是不是平台管理员" = 有没有 `edit_magic_block`。
69
-
70
- 🔴 **不是** Django 的 `is_staff`(那一列只留给 Django 自己的 `/admin`)、
71
- **不是**某个组名(组名只是标签)。`ctx.user.is_admin` / 管理台写口 /
72
- `Flow.admin_bypass` / `/auth/me.isAdmin` 全走这一处。
73
- """
74
- if user is None or not getattr(user, 'is_authenticated', True):
75
- return False
76
- return has_perm(user, ADMIN_PERMISSION)
77
-
78
-
79
- def admin_group() -> Any:
80
- """**持有 `edit_magic_block` 的那个组**(启动时那份管理员名单加人的落点)。
64
+ def group_holding(permission: str) -> Any:
65
+ """**持有这个权限的那个组**(启动时那份管理员名单加人的落点)。
81
66
 
82
- 🔴 按**权限**找、不按组名找:项目把"平台管理员"改名之后这份名单照样能用(组名只是标签)。
67
+ 🔴 按**权限**找、不按组名找:项目把那个组改名之后这份名单照样能用(组名只是标签)。
83
68
  🔴 有多个组都持有它时取主键最小的那个(确定性):名单只往**一个**组里加人。
84
69
  ⚠️ 表还没迁移 / 还没有那个组 ⇒ `None`:调用方静默跳过,别在启动路径上抛。
85
70
  """
86
- app_label, codename = split_permission(ADMIN_PERMISSION)
71
+ app_label, codename = split_permission(permission)
87
72
  if not app_label:
88
73
  return None
89
74
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 3.0.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "labull-framework"
7
- version = "3.0.0"
7
+ version = "3.0.1"
8
8
  description = "Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks"
9
9
  readme = "README-pypi.md"
10
10
  requires-python = ">=3.10"