Metadata-Version: 2.4
Name: labull-framework
Version: 1.0.0
Summary: Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks
Author: Labull Framework
License-Expression: MIT
Keywords: django,authing,oidc,sso,magic-block
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=4.2
Requires-Dist: PyJWT[crypto]>=2.8
Dynamic: license-file

# labull-framework

一个可插拔的 **Django app**（import 名 `labull_framework`），提供两件事：

- **Authing / OIDC 登录**：自带的认证后端，只用标准库 + PyJWT，不依赖任何 OIDC 客户端包。
- **magic block**：一个 uuid + 一整个文件包就是一个功能块，住在数据库里、在沙箱 iframe 里渲染，
  不重新编译、不重启；块可以带自己的后端 Python（`backend_open.py`）。

另附一个可选的 **数据模型页**（`LABULL_SCHEMA_CANVAS=on` 才开）。

## 安装

```bash
pip install labull-framework
```

## 接进一个 Django 项目

`settings.py` 里三件事：

```python
from labull_framework.env import load_env_file
load_env_file(BASE_DIR / '.env')            # ① 第一行：读 .env

AUTH_USER_MODEL = 'labull_framework.User'   # ② 第一次 migrate 之前就要设好

from labull_framework.settings import apply_platform_settings
apply_platform_settings(globals())          # ③ 最后一行：把平台的设置补进来
```

`urls.py` 里一行：

```python
from labull_framework.urls import urlpatterns as labull_urls
urlpatterns = [*labull_urls, path('admin/', admin.site.urls)]
```

`wsgi.py` 里一行（**别漏**，重设管理员名单与启动体检都在这里）：

```python
from labull_framework.magic_block.health import startup_once
startup_once()
```

然后：

```bash
python manage.py migrate
python manage.py runserver
```

`.env` 最少要这几项：

```ini
DATABASE_URL=postgres://postgres:postgres@localhost:5432/app
SSO_ISSUER=https://<你的域名>.authing.cn/oidc
SSO_CLIENT_ID=<Authing 应用 ID>
SSO_CLIENT_SECRET=<Authing 应用密钥>
SSO_REDIRECT_URI=http://localhost:8000/auth/callback
SSO_WEB_ORIGIN=http://localhost:5173
CORS_ORIGIN=http://localhost:5173
```

`sqlite:///db.sqlite3` 也认，本地开发不必装数据库。

| 想要什么 | 怎么做 |
|---|---|
| **谁是管理员** | 配 `BOOTSTRAP_ADMIN_WORK_IDS=工号,工号` —— 它是**声明式**的名单：启动时名单里的人补上 `is_staff`，**名单之外的一律清掉**（没配就是"一个管理员都没有"）。它不碰 `is_superuser`，所以别拿超管当后门 |
| **看数据模型那一页** | 设 `LABULL_SCHEMA_CANVAS=on`，浏览器打开 `/` |
| **让块的依赖声明拦住保存** | 设 `LABULL_REQUIRES=error`（默认 `warn`：判据照跑、**只提示不拦保存**；`off` 整段不跑） |
| **导出一份模型形状** | `python manage.py labull_contract --out .labull/contract.json` |

## magic block

一个块 = **一个 uuid + 一整个文件包**：

```
我的块/
├─ index.open.html        入口（必备）
├─ magic-block.open.yaml  清单（必备）：`id` / `module` / `name` / `brief` 四个键必填
│                         + `requires`（依赖声明：用了哪些模型 / pip 包 / settings 键 / `BLOCK_*`）可选
├─ README.open.md         **业务功能说明**（Markdown，可选）：这一段的家就是这份文件，不在清单里
├─ requires.open.yaml     依赖声明的**草稿**（导出铺下来的：正文进清单的 `requires`，注释是扫代码的建议）
├─ 任意 js / css
├─ excluded/_*            你自己的地盘（测试 / 草稿）：**照样进库，执行时忽略**
└─ backend_open.py        块自己的后端（可选）
```

测试 / 草稿 / 参考实现放块里的 `excluded/`，名字以 `_` 开头（`_test.py` / `_test.mjs`）：
判据只有一条 —— **路径里任何一段以 `_` 开头**的一律照样进库、但执行时被忽略（不加载、
不扫路由、不试加载、不引用深检、也不给页面）。平台不会替你跑它们，自己 `node` / `python` 跑。

块后端与项目里别的 Python 代码没有区别：

```python
from labull_framework.magic_block.sdk import MagicBackendAPI, errors

class Order:
    @MagicBackendAPI()                       # 省略 permission = 任何登录用户都能调
    def list(self, ctx, params):
        from modules.orders.models import Order
        return [{'id': o.id} for o in Order.objects.all()[:20]]

    @MagicBackendAPI(permission='labull_framework.edit_blocks')
    def close(self, ctx, params):
        if not params.get('id'):
            raise errors.BadRequest('要给我一个 id')
        return {'ok': True}
```

配置：环境变量里**带 `BLOCK_` 前缀**的才会进 `ctx.config`（`BLOCK_OCR_KEY=a` ⇒ `ctx.config['OCR_KEY']`）。

## 虚拟类（`@virtual_class`）

几张表共有同一批列时，写一个**虚拟类**：一个**不建表的抽象模型**，自己那几列会被继承到子表上。

```python
from django.db import models
from labull_framework.virtual import virtual_class, VirtualModel


@virtual_class
class Contact(VirtualModel):
    """联系人：个人与单位共有的那几列。"""
    name = models.CharField('名称', max_length=64)

    class Meta:
        abstract = True          # 🔴 必须有（见下）


class Person(Contact):
    birthday = models.DateField('生日', null=True, blank=True)


class Company(Contact):
    tax_no = models.CharField('税号', max_length=32, blank=True, default='')
```

`Person` / `Company` 的表里**真的有** `name` 这一列（Django 把抽象基类的字段复制进子类）；
而 `makemigrations` **只为这两张具体表建表** —— `Contact` 在库里没有对应的表。

两条会真的出错的判据：

1. **`class Meta: abstract = True` 不能省**（`@virtual_class` 替不了它：Django 在 `ModelBase` 里
   读的是**类体那个 `Meta`**，读完就把非抽象模型登记进 app registry 了）。漏了会让这个类**真的建一张表**，
   所以装饰器发现这件事时**当场抛 `TypeError`** 并把该补的那一行写给你。
2. **`@virtual_class` 写在所有装饰器的最上面**（它执行时那个类必须已经是一个 Django 模型）；
   另外**外键不能指向虚拟类**（抽象模型没有 `objects`，Django 在 `manage.py check` 就拦下）。

`LABULL_SCHEMA_CANVAS=on` 时打开 `/`：虚拟类是一张带 `◇` 的虚线卡片（写着"不建表"与"谁在继承它"），
继承它的表表头挂着「**◇ 泛化 · Contact**」—— 鼠标停一下看泛化名与整条链，点一下跳过去。
改动（含"哪张表的泛化父类变了"）与表一样进画布那份差异。

### 平台自带：WorkflowModel（流转记录）

要记"这件事是谁、什么时候、怎么走过来的"的表，**继承它就行**（不用自己写抽象基类）：

```python
from django.db import models
from labull_framework.models import WorkflowModel


class PurchaseOrder(WorkflowModel):
    """采购申请：全过程记在继承来的 log 那一列里。"""
    title = models.CharField('标题', max_length=128)

    class Meta:
        verbose_name = '采购申请'      # 🔴 一定要自己写（不然继承来的是基类那个名字）
```

它给子表带上三列：`log`（流转日志，JSON 数组）、`created_at`（创建时间）与 `updated_at`（自动时间）。
读写走类上的助手，别在调用点自己拼字典：

```python
WorkflowModel.append_log(row, 'submit', actor=user.name, note='提交审批')
row.save(update_fields=['log'])
WorkflowModel.log_of(row)              # → [{'at': ..., 'action': 'submit', ...}, ...]
WorkflowModel.last_log(row, 'approve') # → 最后一次批准（没有就 None）
```

画布上它是一张带 `◇` 的**虚拟卡片**（在 `labull_framework` 模块里），
继承它的表表头挂着「◇ 泛化 · WorkflowModel」。

- **当前状态用列**（要能筛选、能索引），`log` 只回答"怎么走到这一步的"。
- `log` 是**一列 JSON、不是一张子表**（刻意取舍）：代价是跨表查流水要全表扫 JSON ——
  真到那一步就该给它建一张真表。
- 🔴 **块后端不要 import 它**（平台内部模型），开发包的清单里也不会给出那句 import 提示。

## 运行环境

- Python ≥ 3.10
- Django ≥ 4.2（在 6.1 上验证过）
- PyJWT（带 `crypto` 后端）

## License

MIT
