forked from erp-dev/erp
feat: query all process nodes for business_object
This commit is contained in:
177
CHANGELOG_process_nodes.md
Normal file
177
CHANGELOG_process_nodes.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# 新增功能:获取业务对象流程节点接口
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
为 Stateflow 模块新增了获取业务对象所属流程的所有状态节点的功能。
|
||||||
|
|
||||||
|
## 变更内容
|
||||||
|
|
||||||
|
### 1. Services 层 (`stateflow/services.py`)
|
||||||
|
|
||||||
|
新增函数 `get_process_nodes(business_object)`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_process_nodes(business_object: 'models.BusinessObject') -> List[dict]:
|
||||||
|
"""
|
||||||
|
获取业务对象所属流程的所有状态节点列表
|
||||||
|
|
||||||
|
参数:
|
||||||
|
business_object: BusinessObject 实例
|
||||||
|
|
||||||
|
返回:
|
||||||
|
节点列表,每个元素包含:
|
||||||
|
{
|
||||||
|
'id': ProcessNode ID,
|
||||||
|
'state': State对象,
|
||||||
|
'state_id': State ID,
|
||||||
|
'state_name': State 名称,
|
||||||
|
'order': 节点顺序号
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
**用途**: 获取完整的流程节点列表,包含节点顺序和状态信息。
|
||||||
|
|
||||||
|
### 2. API 层 (`api_v1/views/stateflow/business_object.py`)
|
||||||
|
|
||||||
|
新增 API 端点:
|
||||||
|
|
||||||
|
- **URL**: `GET /api/v1/stateflow/business-objects/{id}/process-nodes/`
|
||||||
|
- **描述**: 获取指定业务对象所属流程的所有状态节点
|
||||||
|
- **响应格式**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 3,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"state_id": 10,
|
||||||
|
"state_name": "质检",
|
||||||
|
"order": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"state_id": 11,
|
||||||
|
"state_name": "包装",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"state_id": 12,
|
||||||
|
"state_name": "发货",
|
||||||
|
"order": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 测试覆盖
|
||||||
|
|
||||||
|
#### 功能测试 (`stateflow/tests/test_services.py`)
|
||||||
|
|
||||||
|
新增测试: `test_get_process_nodes`
|
||||||
|
|
||||||
|
- ✅ 验证节点数量正确
|
||||||
|
- ✅ 验证节点顺序正确
|
||||||
|
- ✅ 验证节点字段完整性
|
||||||
|
- ✅ 验证每个节点包含必要字段
|
||||||
|
|
||||||
|
#### API 测试 (`stateflow/tests/test_business_object_api.py`)
|
||||||
|
|
||||||
|
新增测试: `test_get_process_nodes`
|
||||||
|
|
||||||
|
- ✅ 验证 HTTP 200 响应
|
||||||
|
- ✅ 验证响应包含 count 和 nodes 字段
|
||||||
|
- ✅ 验证节点数据格式正确
|
||||||
|
- ✅ 验证节点顺序和内容准确
|
||||||
|
|
||||||
|
### 4. 文档更新
|
||||||
|
|
||||||
|
#### API 文档 (`stateflow/API.md`)
|
||||||
|
|
||||||
|
新增章节: **3.7.5. 获取流程的所有节点**
|
||||||
|
|
||||||
|
- 📝 完整的接口说明
|
||||||
|
- 📝 请求/响应示例
|
||||||
|
- 📝 Python/JavaScript/curl 使用示例
|
||||||
|
|
||||||
|
## 测试结果
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 功能测试
|
||||||
|
$ python manage.py test stateflow.tests.test_services.StateFlowServicesTestCase.test_get_process_nodes
|
||||||
|
✅ OK
|
||||||
|
|
||||||
|
# API 测试
|
||||||
|
$ python manage.py test stateflow.tests.test_business_object_api.BusinessObjectAPITestCase.test_get_process_nodes
|
||||||
|
✅ OK
|
||||||
|
|
||||||
|
# 完整测试套件
|
||||||
|
$ python manage.py test stateflow
|
||||||
|
✅ Ran 80 tests in 11.122s - OK
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
headers = {'Authorization': f'Bearer {token}'}
|
||||||
|
response = requests.get(
|
||||||
|
'http://localhost:8000/api/v1/stateflow/business-objects/1/process-nodes/',
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
print(f"流程共有 {data['count']} 个节点:")
|
||||||
|
for node in data['nodes']:
|
||||||
|
print(f" 节点 {node['order']}: {node['state_name']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/v1/stateflow/business-objects/${businessObjectId}/process-nodes/`,
|
||||||
|
{ headers: { 'Authorization': `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`流程共有 ${data.count} 个节点:`, data.nodes);
|
||||||
|
```
|
||||||
|
|
||||||
|
### curl
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:8000/api/v1/stateflow/business-objects/1/process-nodes/" \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 应用场景
|
||||||
|
|
||||||
|
1. **前端流程可视化**: 获取完整的流程节点列表用于渲染流程图
|
||||||
|
2. **进度追踪**: 结合 timeline 接口展示完整的流程进度
|
||||||
|
3. **流程分析**: 分析流程包含的所有节点和顺序
|
||||||
|
4. **文档生成**: 自动生成流程说明文档
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
- ✅ 完全向后兼容
|
||||||
|
- ✅ 不影响现有 API
|
||||||
|
- ✅ 所有现有测试通过 (80/80)
|
||||||
|
- ✅ 新增测试覆盖完整
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `stateflow/services.py` - 新增 `get_process_nodes()` 函数
|
||||||
|
- `api_v1/views/stateflow/business_object.py` - 新增 `process_nodes()` action
|
||||||
|
- `stateflow/tests/test_services.py` - 新增功能测试
|
||||||
|
- `stateflow/tests/test_business_object_api.py` - 新增 API 测试
|
||||||
|
- `stateflow/API.md` - 更新文档
|
||||||
|
- `examples/test_process_nodes_api.py` - 使用示例
|
||||||
|
|
||||||
|
## 完成日期
|
||||||
|
|
||||||
|
2025-11-17
|
||||||
@@ -285,6 +285,29 @@ class BusinessObjectViewSet(viewsets.ModelViewSet):
|
|||||||
'count': len(parameters)
|
'count': len(parameters)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['get'], url_path='process-nodes')
|
||||||
|
def process_nodes(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
获取业务对象所属流程的所有状态节点
|
||||||
|
|
||||||
|
返回流程中所有节点的列表,按顺序排列
|
||||||
|
"""
|
||||||
|
business_object = self.get_object()
|
||||||
|
nodes = services.get_process_nodes(business_object)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'count': len(nodes),
|
||||||
|
'nodes': [
|
||||||
|
{
|
||||||
|
'id': node['id'],
|
||||||
|
'state_id': node['state_id'],
|
||||||
|
'state_name': node['state_name'],
|
||||||
|
'order': node['order']
|
||||||
|
}
|
||||||
|
for node in nodes
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
@action(detail=True, methods=['post'], url_path='state-logs/(?P<log_id>[^/.]+)/add-parameters')
|
@action(detail=True, methods=['post'], url_path='state-logs/(?P<log_id>[^/.]+)/add-parameters')
|
||||||
def add_parameters_to_log(self, request, pk=None, log_id=None):
|
def add_parameters_to_log(self, request, pk=None, log_id=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
425
api_v1/views/upload/API.md
Normal file
425
api_v1/views/upload/API.md
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
# 文件上传接口文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
通用文件上传接口,用于上传无法归类到具体业务的文件。
|
||||||
|
|
||||||
|
**基础路径**: `/api/v1/upload/`
|
||||||
|
|
||||||
|
**认证要求**: 所有接口都需要 JWT Token 认证
|
||||||
|
|
||||||
|
**内容格式**: `multipart/form-data` (上传时) / `application/json` (响应)
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
- 不支持列表查询(list)
|
||||||
|
- 不支持修改操作(PUT/PATCH)
|
||||||
|
- 仅支持单个文件查询、上传、删除操作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
### UploadedFile
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | integer | 文件ID |
|
||||||
|
| path | string | 文件存储路径(随机文件名) |
|
||||||
|
| file_url | string | 文件访问URL |
|
||||||
|
| owner | integer | 上传者用户ID |
|
||||||
|
| owner_username | string | 上传者用户名 |
|
||||||
|
| is_deleted | boolean | 是否已删除(软删除标记) |
|
||||||
|
| original_filename | string | 原始文件名 |
|
||||||
|
| file_size | integer | 文件大小(字节) |
|
||||||
|
| content_type | string | MIME类型(如 image/jpeg) |
|
||||||
|
| created_at | datetime | 创建时间 |
|
||||||
|
| updated_at | datetime | 更新时间 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口列表
|
||||||
|
|
||||||
|
### 1. 上传文件
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
```
|
||||||
|
POST /api/v1/upload/
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求参数**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| file | file | 是 | 要上传的文件(最大100MB) |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/api/v1/upload/ \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-F "file=@/path/to/your/file.pdf"
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应** (201 Created)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"path": "uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||||||
|
"file_url": "/media/uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||||||
|
"owner": 1,
|
||||||
|
"owner_username": "admin",
|
||||||
|
"is_deleted": false,
|
||||||
|
"original_filename": "document.pdf",
|
||||||
|
"file_size": 1048576,
|
||||||
|
"content_type": "application/pdf",
|
||||||
|
"created_at": "2025-11-17T10:30:00Z",
|
||||||
|
"updated_at": "2025-11-17T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应** (400 Bad Request)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"file": ["未上传文件"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"file": ["文件大小不能超过100MB"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**安全特性**:
|
||||||
|
- 文件名使用 UUID 随机化,防止文件名冲突和路径遍历攻击
|
||||||
|
- 原始文件名保存在数据库中,不影响存储安全
|
||||||
|
- 自动记录上传者信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 获取文件信息
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
```
|
||||||
|
GET /api/v1/upload/{id}/
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | integer | 是 | 文件ID |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
```bash
|
||||||
|
curl -X GET http://localhost:8000/api/v1/upload/1/ \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应** (200 OK)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"path": "uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||||||
|
"file_url": "/media/uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||||||
|
"owner": 1,
|
||||||
|
"owner_username": "admin",
|
||||||
|
"is_deleted": false,
|
||||||
|
"original_filename": "document.pdf",
|
||||||
|
"file_size": 1048576,
|
||||||
|
"content_type": "application/pdf",
|
||||||
|
"created_at": "2025-11-17T10:30:00Z",
|
||||||
|
"updated_at": "2025-11-17T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应** (404 Not Found)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "未找到"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**: 已软删除的文件无法通过此接口查询
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 软删除文件
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
```
|
||||||
|
DELETE /api/v1/upload/{id}/
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | integer | 是 | 文件ID |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
```bash
|
||||||
|
curl -X DELETE http://localhost:8000/api/v1/upload/1/ \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应** (200 OK)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "文件已标记为删除"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 软删除不会物理删除文件,只是标记为已删除
|
||||||
|
- 软删除后的文件无法通过常规接口查询
|
||||||
|
- 可以通过恢复接口恢复文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 恢复已删除文件
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
```
|
||||||
|
POST /api/v1/upload/{id}/restore/
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | integer | 是 | 文件ID |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/api/v1/upload/1/restore/ \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应** (200 OK)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "文件已恢复",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"path": "uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||||||
|
"file_url": "/media/uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||||||
|
"owner": 1,
|
||||||
|
"owner_username": "admin",
|
||||||
|
"is_deleted": false,
|
||||||
|
"original_filename": "document.pdf",
|
||||||
|
"file_size": 1048576,
|
||||||
|
"content_type": "application/pdf",
|
||||||
|
"created_at": "2025-11-17T10:30:00Z",
|
||||||
|
"updated_at": "2025-11-17T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应** (400 Bad Request)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "文件未被删除,无需恢复"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 永久删除文件
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
```
|
||||||
|
DELETE /api/v1/upload/{id}/permanent_delete/
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | integer | 是 | 文件ID |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
```bash
|
||||||
|
curl -X DELETE http://localhost:8000/api/v1/upload/1/permanent_delete/ \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应** (204 No Content)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "文件已永久删除"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 永久删除会物理删除文件和数据库记录
|
||||||
|
- 此操作不可恢复,请谨慎使用
|
||||||
|
- 建议仅在确认不需要时使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### Python (requests)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
BASE_URL = "http://localhost:8000/api/v1"
|
||||||
|
TOKEN = "your_jwt_token"
|
||||||
|
headers = {"Authorization": f"Bearer {TOKEN}"}
|
||||||
|
|
||||||
|
# 1. 上传文件
|
||||||
|
with open('document.pdf', 'rb') as f:
|
||||||
|
files = {'file': f}
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/upload/",
|
||||||
|
headers=headers,
|
||||||
|
files=files
|
||||||
|
)
|
||||||
|
file_data = response.json()
|
||||||
|
file_id = file_data['id']
|
||||||
|
print(f"上传成功,文件ID: {file_id}")
|
||||||
|
|
||||||
|
# 2. 获取文件信息
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/upload/{file_id}/",
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
print(f"文件信息: {response.json()}")
|
||||||
|
|
||||||
|
# 3. 软删除文件
|
||||||
|
response = requests.delete(
|
||||||
|
f"{BASE_URL}/upload/{file_id}/",
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
print(f"软删除: {response.json()}")
|
||||||
|
|
||||||
|
# 4. 恢复文件
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/upload/{file_id}/restore/",
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
print(f"恢复文件: {response.json()}")
|
||||||
|
|
||||||
|
# 5. 永久删除
|
||||||
|
response = requests.delete(
|
||||||
|
f"{BASE_URL}/upload/{file_id}/permanent_delete/",
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
print(f"永久删除完成")
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript (Axios)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const axios = require('axios');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const BASE_URL = 'http://localhost:8000/api/v1';
|
||||||
|
const TOKEN = 'your_jwt_token';
|
||||||
|
const headers = { Authorization: `Bearer ${TOKEN}` };
|
||||||
|
|
||||||
|
// 1. 上传文件
|
||||||
|
async function uploadFile() {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', fs.createReadStream('document.pdf'));
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${BASE_URL}/upload/`,
|
||||||
|
formData,
|
||||||
|
{ headers: { ...headers, ...formData.getHeaders() } }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('上传成功:', response.data);
|
||||||
|
return response.data.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取文件信息
|
||||||
|
async function getFileInfo(fileId) {
|
||||||
|
const response = await axios.get(
|
||||||
|
`${BASE_URL}/upload/${fileId}/`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
console.log('文件信息:', response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 软删除
|
||||||
|
async function softDelete(fileId) {
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${BASE_URL}/upload/${fileId}/`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
console.log('软删除:', response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 恢复文件
|
||||||
|
async function restore(fileId) {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${BASE_URL}/upload/${fileId}/restore/`,
|
||||||
|
{},
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
console.log('恢复:', response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 永久删除
|
||||||
|
async function permanentDelete(fileId) {
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${BASE_URL}/upload/${fileId}/permanent_delete/`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
console.log('永久删除完成');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误码说明
|
||||||
|
|
||||||
|
| HTTP状态码 | 说明 |
|
||||||
|
|-----------|------|
|
||||||
|
| 200 | 成功 |
|
||||||
|
| 201 | 创建成功 |
|
||||||
|
| 204 | 删除成功(无内容) |
|
||||||
|
| 400 | 请求参数错误 |
|
||||||
|
| 401 | 未认证或认证失败 |
|
||||||
|
| 403 | 无权限 |
|
||||||
|
| 404 | 资源不存在 |
|
||||||
|
| 413 | 文件过大 |
|
||||||
|
| 500 | 服务器内部错误 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **文件大小限制**: 单文件最大100MB,超过此限制会返回400错误
|
||||||
|
2. **文件命名**: 系统自动使用UUID生成随机文件名,原始文件名保存在`original_filename`字段
|
||||||
|
3. **软删除策略**: 建议先使用软删除,确认不需要后再使用永久删除
|
||||||
|
4. **文件访问**: 使用返回的`file_url`字段访问文件
|
||||||
|
5. **权限控制**: 所有接口都需要认证,上传的文件自动关联当前用户
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **不支持的操作**:
|
||||||
|
- ❌ 列表查询 (`GET /api/v1/upload/`)
|
||||||
|
- ❌ 批量上传
|
||||||
|
- ❌ 修改文件 (`PUT/PATCH /api/v1/upload/{id}/`)
|
||||||
|
|
||||||
|
2. **文件存储**:
|
||||||
|
- 文件按日期组织:`uploads/YYYY/MM/DD/`
|
||||||
|
- 文件名使用32位十六进制UUID
|
||||||
|
- 保留原始文件扩展名
|
||||||
|
|
||||||
|
3. **查询限制**:
|
||||||
|
- 默认查询会过滤掉已软删除的文件
|
||||||
|
- 要访问已删除文件,需要通过Django Admin或直接数据库查询
|
||||||
|
|
||||||
|
4. **安全考虑**:
|
||||||
|
- 所有文件名随机化,防止路径遍历攻击
|
||||||
|
- 需要JWT认证
|
||||||
|
- 自动记录上传者信息
|
||||||
154
debug_advance_bug.py
Normal file
154
debug_advance_bug.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
"""
|
||||||
|
调试脚本:检查 advance_to_next_state 是否会导致"前进2步"的bug
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'flower.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from stateflow.models import State, Process, BusinessObject, StateFlowRecord
|
||||||
|
from stateflow.services import advance_to_next_state, get_business_object_current_state, get_completed_node_ids, get_progress_percentage
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 创建测试用户
|
||||||
|
user = User.objects.first()
|
||||||
|
if not user:
|
||||||
|
user = User.objects.create_user(username='debuguser', password='debug123')
|
||||||
|
print(f"创建测试用户: {user.username}")
|
||||||
|
|
||||||
|
# 创建3个状态节点
|
||||||
|
state1 = State.objects.create(name='Debug State 1')
|
||||||
|
state2 = State.objects.create(name='Debug State 2')
|
||||||
|
state3 = State.objects.create(name='Debug State 3')
|
||||||
|
print(f"\n创建状态节点:")
|
||||||
|
print(f" - {state1.name} (ID: {state1.id})")
|
||||||
|
print(f" - {state2.name} (ID: {state2.id})")
|
||||||
|
print(f" - {state3.name} (ID: {state3.id})")
|
||||||
|
|
||||||
|
# 创建流程
|
||||||
|
process = Process.objects.create(name='Debug Process')
|
||||||
|
process.replace_nodes([state1, state2, state3])
|
||||||
|
print(f"\n创建流程: {process.name}")
|
||||||
|
print(f"节点顺序: {state1.name} -> {state2.name} -> {state3.name}")
|
||||||
|
|
||||||
|
# 创建业务对象
|
||||||
|
bo = BusinessObject.objects.create(name='Debug BO', process=process)
|
||||||
|
print(f"\n创建业务对象: {bo.name}")
|
||||||
|
|
||||||
|
# 检查初始状态
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"初始状态检查")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
current = get_business_object_current_state(bo)
|
||||||
|
completed_ids = get_completed_node_ids(bo)
|
||||||
|
progress = get_progress_percentage(bo)
|
||||||
|
logs_count = StateFlowRecord.objects.filter(business_object=bo, is_cancelled=False).count()
|
||||||
|
|
||||||
|
print(f"Current State: {current.name if current else 'None'} (ID: {current.id if current else 'N/A'})")
|
||||||
|
print(f"Completed Node IDs: {completed_ids}")
|
||||||
|
print(f"Progress: {progress:.2f}%")
|
||||||
|
print(f"Log Records Count: {logs_count}")
|
||||||
|
print(f"\n✓ 期望: Current='{state1.name}', Completed=[], Progress=0%, Logs=0")
|
||||||
|
|
||||||
|
if current and current.id != state1.id:
|
||||||
|
print(f"❌ 错误: Current State 应该是 '{state1.name}' 但实际是 '{current.name}'")
|
||||||
|
cleanup(bo, process, [state1, state2, state3])
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(completed_ids) != 0:
|
||||||
|
print(f"❌ 错误: 应该没有完成的节点,但有 {len(completed_ids)} 个")
|
||||||
|
cleanup(bo, process, [state1, state2, state3])
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ 初始状态正确")
|
||||||
|
|
||||||
|
# 第一次推进
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"第一次调用 advance_to_next_state")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
success, message, log = advance_to_next_state(bo, user)
|
||||||
|
|
||||||
|
print(f"Success: {success}")
|
||||||
|
print(f"Message: {message}")
|
||||||
|
print(f"Created Log ID: {log.id if log else 'None'}")
|
||||||
|
if log:
|
||||||
|
print(f"Log State: {log.state.name} (ID: {log.state_id})")
|
||||||
|
|
||||||
|
# 检查推进后的状态
|
||||||
|
print(f"\n推进后状态检查:")
|
||||||
|
current = get_business_object_current_state(bo)
|
||||||
|
completed_ids = get_completed_node_ids(bo)
|
||||||
|
progress = get_progress_percentage(bo)
|
||||||
|
logs_count = StateFlowRecord.objects.filter(business_object=bo, is_cancelled=False).count()
|
||||||
|
all_logs = StateFlowRecord.objects.filter(business_object=bo, is_cancelled=False).values_list('state_id', 'state__name')
|
||||||
|
|
||||||
|
print(f"Current State: {current.name if current else 'None'} (ID: {current.id if current else 'N/A'})")
|
||||||
|
print(f"Completed Node IDs: {completed_ids}")
|
||||||
|
print(f"Completed Nodes: {[State.objects.get(id=cid).name for cid in completed_ids]}")
|
||||||
|
print(f"Progress: {progress:.2f}%")
|
||||||
|
print(f"Log Records Count: {logs_count}")
|
||||||
|
print(f"All Logs: {list(all_logs)}")
|
||||||
|
|
||||||
|
print(f"\n✓ 期望: Current='{state2.name}', Completed=[{state1.name}], Progress=33.33%, Logs=1")
|
||||||
|
|
||||||
|
# 验证
|
||||||
|
has_error = False
|
||||||
|
|
||||||
|
if current and current.id != state2.id:
|
||||||
|
print(f"\n❌ BUG FOUND! Current State 应该是 '{state2.name}' (ID: {state2.id}) 但实际是 '{current.name}' (ID: {current.id})")
|
||||||
|
has_error = True
|
||||||
|
|
||||||
|
if len(completed_ids) != 1:
|
||||||
|
print(f"\n❌ BUG FOUND! 应该完成1个节点,但实际完成了 {len(completed_ids)} 个")
|
||||||
|
has_error = True
|
||||||
|
|
||||||
|
if completed_ids and completed_ids[0] != state1.id:
|
||||||
|
print(f"\n❌ BUG FOUND! 完成的节点应该是 '{state1.name}' 但实际是 ID {completed_ids[0]}")
|
||||||
|
has_error = True
|
||||||
|
|
||||||
|
if logs_count != 1:
|
||||||
|
print(f"\n❌ BUG FOUND! 应该有1条日志记录,但实际有 {logs_count} 条")
|
||||||
|
has_error = True
|
||||||
|
|
||||||
|
if abs(progress - 33.33) > 0.1:
|
||||||
|
print(f"\n❌ BUG FOUND! 进度应该是 33.33% 但实际是 {progress:.2f}%")
|
||||||
|
has_error = True
|
||||||
|
|
||||||
|
if not has_error:
|
||||||
|
print("\n✅ 所有检查通过!没有发现bug")
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
cleanup(bo, process, [state1, state2, state3])
|
||||||
|
|
||||||
|
return not has_error
|
||||||
|
|
||||||
|
def cleanup(bo, process, states):
|
||||||
|
"""清理测试数据"""
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print("清理测试数据...")
|
||||||
|
bo.delete()
|
||||||
|
process.delete()
|
||||||
|
for state in states:
|
||||||
|
state.delete()
|
||||||
|
print("清理完成")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
result = main()
|
||||||
|
if result:
|
||||||
|
print("\n🎉 测试通过!")
|
||||||
|
exit(0)
|
||||||
|
else:
|
||||||
|
print("\n💥 测试失败!发现bug")
|
||||||
|
exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 测试异常: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
exit(1)
|
||||||
35
examples/test_process_nodes_api.py
Normal file
35
examples/test_process_nodes_api.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
测试新增的 get_process_nodes API 端点
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:8000"
|
||||||
|
|
||||||
|
# 假设你已经有了 JWT token
|
||||||
|
TOKEN = "your_jwt_token_here"
|
||||||
|
headers = {"Authorization": f"Bearer {TOKEN}"}
|
||||||
|
|
||||||
|
# 业务对象 ID
|
||||||
|
business_object_id = 1
|
||||||
|
|
||||||
|
# 获取流程的所有节点
|
||||||
|
response = requests.get(
|
||||||
|
f"{BASE_URL}/api/v1/stateflow/business-objects/{business_object_id}/process-nodes/",
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(f"流程共有 {data['count']} 个节点:")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for node in data['nodes']:
|
||||||
|
print(f"节点 {node['order'] + 1}:")
|
||||||
|
print(f" - ProcessNode ID: {node['id']}")
|
||||||
|
print(f" - State ID: {node['state_id']}")
|
||||||
|
print(f" - State 名称: {node['state_name']}")
|
||||||
|
print(f" - 顺序: {node['order']}")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
print(f"错误: {response.status_code}")
|
||||||
|
print(response.json())
|
||||||
@@ -268,6 +268,37 @@ Authorization: Bearer <access_token>
|
|||||||
- **描述**: 获取当前待执行节点(`current_state`)的参数模板。
|
- **描述**: 获取当前待执行节点(`current_state`)的参数模板。
|
||||||
- **注意**: `current_state` 指的是下一个待执行的节点。如果流程已完成,则返回空。
|
- **注意**: `current_state` 指的是下一个待执行的节点。如果流程已完成,则返回空。
|
||||||
|
|
||||||
|
#### 3.7.5. 获取流程的所有节点
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/business-objects/{id}/process-nodes/`
|
||||||
|
- **描述**: 获取业务对象所属流程的所有状态节点列表,按顺序排列。
|
||||||
|
- **响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 3,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"state_id": 10,
|
||||||
|
"state_name": "质检",
|
||||||
|
"order": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"state_id": 11,
|
||||||
|
"state_name": "包装",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"state_id": 12,
|
||||||
|
"state_name": "发货",
|
||||||
|
"order": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 3.8. 参数与日志接口 (Custom Actions)
|
### 3.8. 参数与日志接口 (Custom Actions)
|
||||||
|
|
||||||
#### 3.8.1. 获取状态流转记录列表
|
#### 3.8.1. 获取状态流转记录列表
|
||||||
@@ -737,7 +768,7 @@ token = response.json()['access']
|
|||||||
# 创建状态
|
# 创建状态
|
||||||
headers = {'Authorization': f'Bearer {token}'}
|
headers = {'Authorization': f'Bearer {token}'}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
'http://localhost:8000/api/v1/states/',
|
'http://localhost:8000/api/v1/stateflow/states/',
|
||||||
json={
|
json={
|
||||||
'name': '待审核',
|
'name': '待审核',
|
||||||
'description': '等待审核'
|
'description': '等待审核'
|
||||||
@@ -745,26 +776,50 @@ response = requests.post(
|
|||||||
headers=headers
|
headers=headers
|
||||||
)
|
)
|
||||||
state = response.json()
|
state = response.json()
|
||||||
|
|
||||||
|
# 获取业务对象的流程节点
|
||||||
|
business_object_id = 1
|
||||||
|
response = requests.get(
|
||||||
|
f'http://localhost:8000/api/v1/stateflow/business-objects/{business_object_id}/process-nodes/',
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
nodes_data = response.json()
|
||||||
|
print(f"流程共有 {nodes_data['count']} 个节点")
|
||||||
|
for node in nodes_data['nodes']:
|
||||||
|
print(f" 节点 {node['order']}: {node['state_name']}")
|
||||||
```
|
```
|
||||||
|
|
||||||
### JavaScript (fetch)
|
### JavaScript (fetch)
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 获取状态列表
|
// 获取状态列表
|
||||||
const response = await fetch('/api/v1/states/?limit=10&offset=0', {
|
const response = await fetch('/api/v1/stateflow/states/?limit=10&offset=0', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log(data.results);
|
console.log(data.results);
|
||||||
|
|
||||||
|
// 获取业务对象的流程节点
|
||||||
|
const businessObjectId = 1;
|
||||||
|
const nodesResponse = await fetch(
|
||||||
|
`/api/v1/stateflow/business-objects/${businessObjectId}/process-nodes/`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const nodesData = await nodesResponse.json();
|
||||||
|
console.log(`流程共有 ${nodesData.count} 个节点:`, nodesData.nodes);
|
||||||
```
|
```
|
||||||
|
|
||||||
### curl
|
### curl
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建流程
|
# 创建流程
|
||||||
curl -X POST "http://localhost:8000/api/v1/processes/" \
|
curl -X POST "http://localhost:8000/api/v1/stateflow/processes/" \
|
||||||
-H "Authorization: Bearer <token>" \
|
-H "Authorization: Bearer <token>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
@@ -775,4 +830,8 @@ curl -X POST "http://localhost:8000/api/v1/processes/" \
|
|||||||
{"state_id": 2, "order": 1}
|
{"state_id": 2, "order": 1}
|
||||||
]
|
]
|
||||||
}'
|
}'
|
||||||
|
|
||||||
|
# 获取业务对象的流程节点
|
||||||
|
curl -X GET "http://localhost:8000/api/v1/stateflow/business-objects/1/process-nodes/" \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -476,3 +476,34 @@ def add_parameters_to_state_log(state_log: 'models.StateFlowRecord', remark: str
|
|||||||
StateLogParameterRecord: 创建的参数记录
|
StateLogParameterRecord: 创建的参数记录
|
||||||
"""
|
"""
|
||||||
return create_parameter_record(state_log, remark, **parameters)
|
return create_parameter_record(state_log, remark, **parameters)
|
||||||
|
|
||||||
|
|
||||||
|
def get_process_nodes(business_object: 'models.BusinessObject') -> List[dict]:
|
||||||
|
"""
|
||||||
|
获取业务对象所属流程的所有状态节点列表
|
||||||
|
|
||||||
|
参数:
|
||||||
|
business_object: BusinessObject 实例
|
||||||
|
|
||||||
|
返回:
|
||||||
|
节点列表,每个元素包含:
|
||||||
|
{
|
||||||
|
'id': ProcessNode ID,
|
||||||
|
'state': State对象,
|
||||||
|
'state_id': State ID,
|
||||||
|
'state_name': State 名称,
|
||||||
|
'order': 节点顺序号
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id')
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'id': node.id,
|
||||||
|
'state': node.state,
|
||||||
|
'state_id': node.state_id,
|
||||||
|
'state_name': node.state.name,
|
||||||
|
'order': node.order,
|
||||||
|
}
|
||||||
|
for node in process_nodes
|
||||||
|
]
|
||||||
|
|||||||
@@ -652,3 +652,39 @@ class BusinessObjectAPITestCase(TestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertFalse(response.data['success'])
|
self.assertFalse(response.data['success'])
|
||||||
self.assertIn('流程没有任何节点', response.data['message'])
|
self.assertIn('流程没有任何节点', response.data['message'])
|
||||||
|
|
||||||
|
def test_get_process_nodes(self):
|
||||||
|
"""测试获取业务对象所属流程的所有状态节点 API"""
|
||||||
|
response = self.client.get(
|
||||||
|
f'/api/v1/stateflow/business-objects/{self.business_object.id}/process-nodes/'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('count', response.data)
|
||||||
|
self.assertIn('nodes', response.data)
|
||||||
|
|
||||||
|
# 验证节点数量
|
||||||
|
self.assertEqual(response.data['count'], 3)
|
||||||
|
self.assertEqual(len(response.data['nodes']), 3)
|
||||||
|
|
||||||
|
# 验证节点内容和顺序
|
||||||
|
nodes = response.data['nodes']
|
||||||
|
|
||||||
|
self.assertEqual(nodes[0]['state_id'], self.state1.id)
|
||||||
|
self.assertEqual(nodes[0]['state_name'], self.state1.name)
|
||||||
|
self.assertEqual(nodes[0]['order'], 0)
|
||||||
|
|
||||||
|
self.assertEqual(nodes[1]['state_id'], self.state2.id)
|
||||||
|
self.assertEqual(nodes[1]['state_name'], self.state2.name)
|
||||||
|
self.assertEqual(nodes[1]['order'], 1)
|
||||||
|
|
||||||
|
self.assertEqual(nodes[2]['state_id'], self.state3.id)
|
||||||
|
self.assertEqual(nodes[2]['state_name'], self.state3.name)
|
||||||
|
self.assertEqual(nodes[2]['order'], 2)
|
||||||
|
|
||||||
|
# 验证每个节点都有必要的字段
|
||||||
|
for node in nodes:
|
||||||
|
self.assertIn('id', node)
|
||||||
|
self.assertIn('state_id', node)
|
||||||
|
self.assertIn('state_name', node)
|
||||||
|
self.assertIn('order', node)
|
||||||
|
|||||||
@@ -288,3 +288,33 @@ class StateFlowServicesTestCase(TestCase):
|
|||||||
self.state3.parameters.add(required_param)
|
self.state3.parameters.add(required_param)
|
||||||
params = services.get_state_parameters(self.state3, required_only=True)
|
params = services.get_state_parameters(self.state3, required_only=True)
|
||||||
self.assertEqual(len(params), 1)
|
self.assertEqual(len(params), 1)
|
||||||
|
|
||||||
|
def test_get_process_nodes(self):
|
||||||
|
"""测试获取业务对象所属流程的所有状态节点"""
|
||||||
|
# 获取流程节点
|
||||||
|
nodes = services.get_process_nodes(self.business_object)
|
||||||
|
|
||||||
|
# 验证节点数量
|
||||||
|
self.assertEqual(len(nodes), 3)
|
||||||
|
|
||||||
|
# 验证节点顺序和内容
|
||||||
|
self.assertEqual(nodes[0]['state_id'], self.state1.id)
|
||||||
|
self.assertEqual(nodes[0]['state_name'], self.state1.name)
|
||||||
|
self.assertEqual(nodes[0]['order'], 0)
|
||||||
|
|
||||||
|
self.assertEqual(nodes[1]['state_id'], self.state2.id)
|
||||||
|
self.assertEqual(nodes[1]['state_name'], self.state2.name)
|
||||||
|
self.assertEqual(nodes[1]['order'], 1)
|
||||||
|
|
||||||
|
self.assertEqual(nodes[2]['state_id'], self.state3.id)
|
||||||
|
self.assertEqual(nodes[2]['state_name'], self.state3.name)
|
||||||
|
self.assertEqual(nodes[2]['order'], 2)
|
||||||
|
|
||||||
|
# 验证每个节点都有必要的字段
|
||||||
|
for node in nodes:
|
||||||
|
self.assertIn('id', node)
|
||||||
|
self.assertIn('state', node)
|
||||||
|
self.assertIn('state_id', node)
|
||||||
|
self.assertIn('state_name', node)
|
||||||
|
self.assertIn('order', node)
|
||||||
|
self.assertIsInstance(node['state'], models.State)
|
||||||
|
|||||||
Reference in New Issue
Block a user