django-kv-config-widget 0.1.0__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.
- django_kv_config_widget-0.1.0/LICENSE +21 -0
- django_kv_config_widget-0.1.0/PKG-INFO +235 -0
- django_kv_config_widget-0.1.0/README.md +208 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget/__init__.py +0 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget/apps.py +6 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget/fields.py +33 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget/locale/zh_Hans/LC_MESSAGES/django.po +88 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget/static/django_kv_config_widget/css/django_kv_config_widget.css +50 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget/static/django_kv_config_widget/js/django_kv_config_widget.js +336 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget/templates/django_kv_config_widget/widgets/key_value.html +58 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget/widgets.py +162 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget.egg-info/PKG-INFO +235 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget.egg-info/SOURCES.txt +32 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget.egg-info/dependency_links.txt +1 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget.egg-info/requires.txt +2 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget.egg-info/top_level.txt +3 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_demo/__init__.py +0 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_demo/asgi.py +5 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_demo/settings.py +67 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_demo/urls.py +6 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_demo/wsgi.py +5 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_example/__init__.py +0 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_example/admin.py +40 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_example/apps.py +6 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_example/forms.py +33 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_example/migrations/0001_initial.py +50 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_example/migrations/0002_kvconfigmodel_config2_alter_kvconfigmodel_config.py +25 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_example/migrations/0003_alter_kvconfigmodel_config_and_more.py +36 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_example/migrations/__init__.py +0 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_example/models.py +17 -0
- django_kv_config_widget-0.1.0/django_kv_config_widget_example/views.py +20 -0
- django_kv_config_widget-0.1.0/pyproject.toml +42 -0
- django_kv_config_widget-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rRR0VrFP
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-kv-config-widget
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Django widget for managing key-value configuration pairs with dynamic add/remove support
|
|
5
|
+
Author-email: rRR0VrFP <rrr0vrfp@qq.com>
|
|
6
|
+
Maintainer-email: rRR0VrFP <rrr0vrfp@qq.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: homepage, https://gitee.com/rRR0VrFP/django-kv-config-widget
|
|
9
|
+
Keywords: django,widget,key-value,config,admin
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 4.2
|
|
13
|
+
Classifier: Framework :: Django :: 5.0
|
|
14
|
+
Classifier: Framework :: Django :: 5.1
|
|
15
|
+
Classifier: Framework :: Django :: 5.2
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: Django>=4.2
|
|
25
|
+
Requires-Dist: PyYAML>=6.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# django-kv-config-widget
|
|
29
|
+
|
|
30
|
+
django-kv-config-widget 是一个专为 Django 后台设计的键值对(Key-Value)配置编辑组件。它提供表格化界面,帮助非技术用户直观管理 JSON 配置数据,有效避免手工编辑原始 JSON 可能引发的格式错误。
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+

|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
## 特性
|
|
37
|
+
|
|
38
|
+
- 动态添加/删除行
|
|
39
|
+
- 多行值输入,自动撑高
|
|
40
|
+
- **类型系统** — `str`、`text`、`number`、`int`、`bool`(NullableBoolean)、`json`,自动切换控件,提交时校验类型
|
|
41
|
+
- **`default_keys`** — 预设键名,支持默认值、占位提示、类型
|
|
42
|
+
- **`required_keys`** — 必填键,值为空时阻止提交
|
|
43
|
+
- **`allow_custom`** — 设为 `False` 时键以下拉选择,不可重复,所有键用完后自动隐藏添加按钮
|
|
44
|
+
- **导入/导出** — 粘贴 YAML、JSON 或 `KEY=VALUE` 文本;一键导出为 YAML
|
|
45
|
+
- **Enter** 在键输入框跳转到值输入框;在值文本框内换行
|
|
46
|
+
- 国际化(默认英文,设置 `LANGUAGE_CODE=zh-hans` 切换中文)
|
|
47
|
+
- Null 安全,跟随 Django CSS 变量主题,系统焦点环
|
|
48
|
+
- 兼容 `JSONField` 和 `TextField`
|
|
49
|
+
- 纯 CSS,无外部依赖
|
|
50
|
+
|
|
51
|
+
## 安装
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install django-kv-config-widget
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
在 `INSTALLED_APPS` 中注册:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
INSTALLED_APPS = [
|
|
61
|
+
...
|
|
62
|
+
"django_kv_config_widget",
|
|
63
|
+
]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 用法
|
|
67
|
+
|
|
68
|
+
### 推荐:使用 KVConfigFormField(自动校验)
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from django_kv_config_widget.fields import KVConfigFormField
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ConfigForm(forms.Form):
|
|
75
|
+
settings = KVConfigFormField(
|
|
76
|
+
label="应用配置",
|
|
77
|
+
default_keys=[
|
|
78
|
+
{"key": "DB_URL", "type": "str", "default": "postgres://localhost:5432/app", "placeholder": "数据库连接地址"},
|
|
79
|
+
{"key": "DEBUG", "type": "bool", "default": "true"},
|
|
80
|
+
{"key": "PORT", "type": "int", "default": "8080"},
|
|
81
|
+
{"key": "ALLOWED_HOSTS", "type": "json", "placeholder": '["localhost", "example.com"]'},
|
|
82
|
+
"LOG_LEVEL",
|
|
83
|
+
],
|
|
84
|
+
required_keys=["DB_URL"],
|
|
85
|
+
allow_custom=False,
|
|
86
|
+
help_text="仅允许预定义的键,DB_URL 为必填项。",
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 直接使用 Widget(手动校验)
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from django import forms
|
|
94
|
+
from django_kv_config_widget.widgets import DjangoKVConfigWidget
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ConfigForm(forms.Form):
|
|
98
|
+
settings = forms.JSONField(
|
|
99
|
+
widget=DjangoKVConfigWidget(
|
|
100
|
+
default_keys=[
|
|
101
|
+
{"key": "DB_URL", "type": "str", "default": "postgres://localhost/app"},
|
|
102
|
+
{"key": "PORT", "type": "int", "default": "8080"},
|
|
103
|
+
],
|
|
104
|
+
required_keys=["DB_URL"],
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def clean_settings(self):
|
|
109
|
+
value = self.cleaned_data.get("settings") or {}
|
|
110
|
+
widget = self.fields["settings"].widget
|
|
111
|
+
DjangoKVConfigWidget.validate_required_keys(value, widget.required_keys)
|
|
112
|
+
DjangoKVConfigWidget.validate_types(value, DjangoKVConfigWidget.get_type_map(widget.default_keys))
|
|
113
|
+
if not widget.allow_custom:
|
|
114
|
+
allowed = {dk["key"] for dk in widget.default_keys}
|
|
115
|
+
for k in value:
|
|
116
|
+
if k not in allowed:
|
|
117
|
+
raise forms.ValidationError("'%s' 不在预定义键中" % k)
|
|
118
|
+
return value
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 在 Django Admin 中使用
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from django.contrib import admin
|
|
125
|
+
from django import forms
|
|
126
|
+
from django_kv_config_widget.fields import KVConfigFormField
|
|
127
|
+
from .models import MyModel
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class MyModelForm(forms.ModelForm):
|
|
131
|
+
config = KVConfigFormField(
|
|
132
|
+
label="配置",
|
|
133
|
+
default_keys=[{"key": "DB_URL", "type": "str", "default": "postgres://localhost/app"}],
|
|
134
|
+
required_keys=["DB_URL"],
|
|
135
|
+
allow_custom=False,
|
|
136
|
+
)
|
|
137
|
+
class Meta:
|
|
138
|
+
model = MyModel
|
|
139
|
+
fields = "__all__"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@admin.register(MyModel)
|
|
143
|
+
class MyModelAdmin(admin.ModelAdmin):
|
|
144
|
+
form = MyModelForm
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `default_keys` 格式
|
|
148
|
+
|
|
149
|
+
| 形式 | 示例 | 效果 |
|
|
150
|
+
|------|------|------|
|
|
151
|
+
| `str` | `"LOG_LEVEL"` | 文本输入框,无默认值 |
|
|
152
|
+
| `dict` | `{"key":"PORT","type":"int","default":"8080","placeholder":"端口号"}` | 类型化输入,有默认值 + 占位提示 |
|
|
153
|
+
|
|
154
|
+
### 类型对照
|
|
155
|
+
|
|
156
|
+
| 类型 | 控件 | 值 |
|
|
157
|
+
|------|------|-----|
|
|
158
|
+
| `str` | textarea(单行外观) | 任意文本,支持换行 |
|
|
159
|
+
| `text` | textarea | 任意文本,支持换行 |
|
|
160
|
+
| `number` | `<input type="number">` | 浮点数 |
|
|
161
|
+
| `int` | `<input type="number" step="1">` | 整数 |
|
|
162
|
+
| `bool` | `<select>` | 空 / `true` / `false`(三态) |
|
|
163
|
+
| `json` | textarea | 合法 YAML/JSON |
|
|
164
|
+
|
|
165
|
+
### 导入 / 导出
|
|
166
|
+
|
|
167
|
+
点击 **Import** 粘贴 YAML、JSON 或 `KEY=VALUE` 文本,自动解析填充表格。
|
|
168
|
+
点击 **Export** 将当前数据导出为 YAML。
|
|
169
|
+
|
|
170
|
+
### 国际化
|
|
171
|
+
|
|
172
|
+
默认界面为英文。设置以下内容可切换为中文:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
LANGUAGE_CODE = "zh-hans"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
校验错误信息和占位提示均已翻译。
|
|
179
|
+
|
|
180
|
+
## API
|
|
181
|
+
|
|
182
|
+
### `DjangoKVConfigWidget`
|
|
183
|
+
|
|
184
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
185
|
+
|------|------|--------|------|
|
|
186
|
+
| `default_keys` | `list[str \| dict]` | `[]` | 预定义键。`str` 为简单键名;`dict` 支持 `key`、`default`、`placeholder`、`type` |
|
|
187
|
+
| `required_keys` | `list[str]` | `[]` | 必填键列表,值为空时提交不通过 |
|
|
188
|
+
| `allow_custom` | `bool` | `True` | 是否允许自定义键。`False` 时键以下拉选择,不可重复,用完后隐藏添加按钮 |
|
|
189
|
+
| `attrs` | `dict` | `None` | 标准 Django widget HTML 属性 |
|
|
190
|
+
|
|
191
|
+
静态方法:
|
|
192
|
+
- `validate_required_keys(value, required_keys)` — 必填键为空时抛出 `ValidationError`
|
|
193
|
+
- `validate_types(value, type_map)` — 类型不匹配时抛出 `ValidationError`
|
|
194
|
+
- `get_type_map(default_keys)` → `dict` — 构建 `{键名: 类型}` 映射
|
|
195
|
+
|
|
196
|
+
### `KVConfigFormField(forms.JSONField)`
|
|
197
|
+
|
|
198
|
+
自动校验必填键、类型、自定义键限制。接收 `default_keys`、`required_keys`、`allow_custom` 参数(透传给内部 widget)。
|
|
199
|
+
|
|
200
|
+
## 数据格式
|
|
201
|
+
|
|
202
|
+
提交时以 JSON 对象形式存储。空键被丢弃。空提交返回 `""`。
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{"KEY1": "value1", "KEY2": "value2"}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## 开发
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
git clone ...
|
|
212
|
+
cd django-kv-config-widget
|
|
213
|
+
pip install -r requirements.txt
|
|
214
|
+
python manage.py migrate
|
|
215
|
+
python manage.py createsuperuser
|
|
216
|
+
python manage.py runserver
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- 管理后台:http://127.0.0.1:8000/admin/
|
|
220
|
+
|
|
221
|
+
## 更新记录
|
|
222
|
+
|
|
223
|
+
### v0.1.0
|
|
224
|
+
|
|
225
|
+
1. 动态添加/删除行,多行值自动撑高
|
|
226
|
+
2. `default_keys` 支持 dict 格式(`default`、`placeholder`、`type`)
|
|
227
|
+
3. 类型系统:`str`、`text`、`number`、`int`、`bool`(NullableBoolean)、`json`
|
|
228
|
+
4. `required_keys` — 必填键为空时阻止提交
|
|
229
|
+
5. `allow_custom` — 预定义键下拉选择;禁止重复;用完后隐藏添加按钮
|
|
230
|
+
6. `KVConfigFormField` — 内置必填 + 类型 + 自定义键校验
|
|
231
|
+
7. 导入/导出 — YAML、JSON、KEY=VALUE
|
|
232
|
+
8. 键盘:Enter 键→值跳转;Enter 值内换行
|
|
233
|
+
9. 国际化(英文默认,中文通过 `LANGUAGE_CODE=zh-hans`)
|
|
234
|
+
10. 系统焦点环、Django CSS 变量主题
|
|
235
|
+
11. `validate_required_keys`、`validate_types`、`get_type_map` 静态方法
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# django-kv-config-widget
|
|
2
|
+
|
|
3
|
+
django-kv-config-widget 是一个专为 Django 后台设计的键值对(Key-Value)配置编辑组件。它提供表格化界面,帮助非技术用户直观管理 JSON 配置数据,有效避免手工编辑原始 JSON 可能引发的格式错误。
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## 特性
|
|
10
|
+
|
|
11
|
+
- 动态添加/删除行
|
|
12
|
+
- 多行值输入,自动撑高
|
|
13
|
+
- **类型系统** — `str`、`text`、`number`、`int`、`bool`(NullableBoolean)、`json`,自动切换控件,提交时校验类型
|
|
14
|
+
- **`default_keys`** — 预设键名,支持默认值、占位提示、类型
|
|
15
|
+
- **`required_keys`** — 必填键,值为空时阻止提交
|
|
16
|
+
- **`allow_custom`** — 设为 `False` 时键以下拉选择,不可重复,所有键用完后自动隐藏添加按钮
|
|
17
|
+
- **导入/导出** — 粘贴 YAML、JSON 或 `KEY=VALUE` 文本;一键导出为 YAML
|
|
18
|
+
- **Enter** 在键输入框跳转到值输入框;在值文本框内换行
|
|
19
|
+
- 国际化(默认英文,设置 `LANGUAGE_CODE=zh-hans` 切换中文)
|
|
20
|
+
- Null 安全,跟随 Django CSS 变量主题,系统焦点环
|
|
21
|
+
- 兼容 `JSONField` 和 `TextField`
|
|
22
|
+
- 纯 CSS,无外部依赖
|
|
23
|
+
|
|
24
|
+
## 安装
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install django-kv-config-widget
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
在 `INSTALLED_APPS` 中注册:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
INSTALLED_APPS = [
|
|
34
|
+
...
|
|
35
|
+
"django_kv_config_widget",
|
|
36
|
+
]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 用法
|
|
40
|
+
|
|
41
|
+
### 推荐:使用 KVConfigFormField(自动校验)
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from django_kv_config_widget.fields import KVConfigFormField
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ConfigForm(forms.Form):
|
|
48
|
+
settings = KVConfigFormField(
|
|
49
|
+
label="应用配置",
|
|
50
|
+
default_keys=[
|
|
51
|
+
{"key": "DB_URL", "type": "str", "default": "postgres://localhost:5432/app", "placeholder": "数据库连接地址"},
|
|
52
|
+
{"key": "DEBUG", "type": "bool", "default": "true"},
|
|
53
|
+
{"key": "PORT", "type": "int", "default": "8080"},
|
|
54
|
+
{"key": "ALLOWED_HOSTS", "type": "json", "placeholder": '["localhost", "example.com"]'},
|
|
55
|
+
"LOG_LEVEL",
|
|
56
|
+
],
|
|
57
|
+
required_keys=["DB_URL"],
|
|
58
|
+
allow_custom=False,
|
|
59
|
+
help_text="仅允许预定义的键,DB_URL 为必填项。",
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 直接使用 Widget(手动校验)
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from django import forms
|
|
67
|
+
from django_kv_config_widget.widgets import DjangoKVConfigWidget
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ConfigForm(forms.Form):
|
|
71
|
+
settings = forms.JSONField(
|
|
72
|
+
widget=DjangoKVConfigWidget(
|
|
73
|
+
default_keys=[
|
|
74
|
+
{"key": "DB_URL", "type": "str", "default": "postgres://localhost/app"},
|
|
75
|
+
{"key": "PORT", "type": "int", "default": "8080"},
|
|
76
|
+
],
|
|
77
|
+
required_keys=["DB_URL"],
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def clean_settings(self):
|
|
82
|
+
value = self.cleaned_data.get("settings") or {}
|
|
83
|
+
widget = self.fields["settings"].widget
|
|
84
|
+
DjangoKVConfigWidget.validate_required_keys(value, widget.required_keys)
|
|
85
|
+
DjangoKVConfigWidget.validate_types(value, DjangoKVConfigWidget.get_type_map(widget.default_keys))
|
|
86
|
+
if not widget.allow_custom:
|
|
87
|
+
allowed = {dk["key"] for dk in widget.default_keys}
|
|
88
|
+
for k in value:
|
|
89
|
+
if k not in allowed:
|
|
90
|
+
raise forms.ValidationError("'%s' 不在预定义键中" % k)
|
|
91
|
+
return value
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 在 Django Admin 中使用
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from django.contrib import admin
|
|
98
|
+
from django import forms
|
|
99
|
+
from django_kv_config_widget.fields import KVConfigFormField
|
|
100
|
+
from .models import MyModel
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class MyModelForm(forms.ModelForm):
|
|
104
|
+
config = KVConfigFormField(
|
|
105
|
+
label="配置",
|
|
106
|
+
default_keys=[{"key": "DB_URL", "type": "str", "default": "postgres://localhost/app"}],
|
|
107
|
+
required_keys=["DB_URL"],
|
|
108
|
+
allow_custom=False,
|
|
109
|
+
)
|
|
110
|
+
class Meta:
|
|
111
|
+
model = MyModel
|
|
112
|
+
fields = "__all__"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@admin.register(MyModel)
|
|
116
|
+
class MyModelAdmin(admin.ModelAdmin):
|
|
117
|
+
form = MyModelForm
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `default_keys` 格式
|
|
121
|
+
|
|
122
|
+
| 形式 | 示例 | 效果 |
|
|
123
|
+
|------|------|------|
|
|
124
|
+
| `str` | `"LOG_LEVEL"` | 文本输入框,无默认值 |
|
|
125
|
+
| `dict` | `{"key":"PORT","type":"int","default":"8080","placeholder":"端口号"}` | 类型化输入,有默认值 + 占位提示 |
|
|
126
|
+
|
|
127
|
+
### 类型对照
|
|
128
|
+
|
|
129
|
+
| 类型 | 控件 | 值 |
|
|
130
|
+
|------|------|-----|
|
|
131
|
+
| `str` | textarea(单行外观) | 任意文本,支持换行 |
|
|
132
|
+
| `text` | textarea | 任意文本,支持换行 |
|
|
133
|
+
| `number` | `<input type="number">` | 浮点数 |
|
|
134
|
+
| `int` | `<input type="number" step="1">` | 整数 |
|
|
135
|
+
| `bool` | `<select>` | 空 / `true` / `false`(三态) |
|
|
136
|
+
| `json` | textarea | 合法 YAML/JSON |
|
|
137
|
+
|
|
138
|
+
### 导入 / 导出
|
|
139
|
+
|
|
140
|
+
点击 **Import** 粘贴 YAML、JSON 或 `KEY=VALUE` 文本,自动解析填充表格。
|
|
141
|
+
点击 **Export** 将当前数据导出为 YAML。
|
|
142
|
+
|
|
143
|
+
### 国际化
|
|
144
|
+
|
|
145
|
+
默认界面为英文。设置以下内容可切换为中文:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
LANGUAGE_CODE = "zh-hans"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
校验错误信息和占位提示均已翻译。
|
|
152
|
+
|
|
153
|
+
## API
|
|
154
|
+
|
|
155
|
+
### `DjangoKVConfigWidget`
|
|
156
|
+
|
|
157
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
158
|
+
|------|------|--------|------|
|
|
159
|
+
| `default_keys` | `list[str \| dict]` | `[]` | 预定义键。`str` 为简单键名;`dict` 支持 `key`、`default`、`placeholder`、`type` |
|
|
160
|
+
| `required_keys` | `list[str]` | `[]` | 必填键列表,值为空时提交不通过 |
|
|
161
|
+
| `allow_custom` | `bool` | `True` | 是否允许自定义键。`False` 时键以下拉选择,不可重复,用完后隐藏添加按钮 |
|
|
162
|
+
| `attrs` | `dict` | `None` | 标准 Django widget HTML 属性 |
|
|
163
|
+
|
|
164
|
+
静态方法:
|
|
165
|
+
- `validate_required_keys(value, required_keys)` — 必填键为空时抛出 `ValidationError`
|
|
166
|
+
- `validate_types(value, type_map)` — 类型不匹配时抛出 `ValidationError`
|
|
167
|
+
- `get_type_map(default_keys)` → `dict` — 构建 `{键名: 类型}` 映射
|
|
168
|
+
|
|
169
|
+
### `KVConfigFormField(forms.JSONField)`
|
|
170
|
+
|
|
171
|
+
自动校验必填键、类型、自定义键限制。接收 `default_keys`、`required_keys`、`allow_custom` 参数(透传给内部 widget)。
|
|
172
|
+
|
|
173
|
+
## 数据格式
|
|
174
|
+
|
|
175
|
+
提交时以 JSON 对象形式存储。空键被丢弃。空提交返回 `""`。
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{"KEY1": "value1", "KEY2": "value2"}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 开发
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
git clone ...
|
|
185
|
+
cd django-kv-config-widget
|
|
186
|
+
pip install -r requirements.txt
|
|
187
|
+
python manage.py migrate
|
|
188
|
+
python manage.py createsuperuser
|
|
189
|
+
python manage.py runserver
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- 管理后台:http://127.0.0.1:8000/admin/
|
|
193
|
+
|
|
194
|
+
## 更新记录
|
|
195
|
+
|
|
196
|
+
### v0.1.0
|
|
197
|
+
|
|
198
|
+
1. 动态添加/删除行,多行值自动撑高
|
|
199
|
+
2. `default_keys` 支持 dict 格式(`default`、`placeholder`、`type`)
|
|
200
|
+
3. 类型系统:`str`、`text`、`number`、`int`、`bool`(NullableBoolean)、`json`
|
|
201
|
+
4. `required_keys` — 必填键为空时阻止提交
|
|
202
|
+
5. `allow_custom` — 预定义键下拉选择;禁止重复;用完后隐藏添加按钮
|
|
203
|
+
6. `KVConfigFormField` — 内置必填 + 类型 + 自定义键校验
|
|
204
|
+
7. 导入/导出 — YAML、JSON、KEY=VALUE
|
|
205
|
+
8. 键盘:Enter 键→值跳转;Enter 值内换行
|
|
206
|
+
9. 国际化(英文默认,中文通过 `LANGUAGE_CODE=zh-hans`)
|
|
207
|
+
10. 系统焦点环、Django CSS 变量主题
|
|
208
|
+
11. `validate_required_keys`、`validate_types`、`get_type_map` 静态方法
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
|
|
3
|
+
from .widgets import DjangoKVConfigWidget
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KVConfigFormField(forms.JSONField):
|
|
7
|
+
|
|
8
|
+
def __init__(self, *, default_keys=None, required_keys=None, allow_custom=True, **kwargs):
|
|
9
|
+
widget = kwargs.pop("widget", None) or DjangoKVConfigWidget(
|
|
10
|
+
default_keys=default_keys,
|
|
11
|
+
required_keys=required_keys,
|
|
12
|
+
allow_custom=allow_custom,
|
|
13
|
+
)
|
|
14
|
+
kwargs["widget"] = widget
|
|
15
|
+
super().__init__(**kwargs)
|
|
16
|
+
|
|
17
|
+
def clean(self, value):
|
|
18
|
+
value = super().clean(value)
|
|
19
|
+
required_keys = getattr(self.widget, "required_keys", None)
|
|
20
|
+
default_keys = getattr(self.widget, "default_keys", None) or []
|
|
21
|
+
allow_custom = getattr(self.widget, "allow_custom", True)
|
|
22
|
+
if required_keys and value:
|
|
23
|
+
DjangoKVConfigWidget.validate_required_keys(value, required_keys)
|
|
24
|
+
if value and isinstance(value, dict):
|
|
25
|
+
type_map = DjangoKVConfigWidget.get_type_map(default_keys)
|
|
26
|
+
if type_map:
|
|
27
|
+
DjangoKVConfigWidget.validate_types(value, type_map)
|
|
28
|
+
if not allow_custom:
|
|
29
|
+
allowed = {dk["key"] for dk in default_keys}
|
|
30
|
+
for k in value:
|
|
31
|
+
if k not in allowed:
|
|
32
|
+
raise forms.ValidationError("'%s' is not in the predefined keys" % k)
|
|
33
|
+
return value
|
|
Binary file
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Chinese (Simplified) translations for django-kv-config-widget
|
|
2
|
+
# Copyright (C) 2025 rRR0VrFP
|
|
3
|
+
#
|
|
4
|
+
msgid ""
|
|
5
|
+
msgstr ""
|
|
6
|
+
"Project-Id-Version: django-kv-config-widget 0.1.0\n"
|
|
7
|
+
"Report-Msgid-Bugs-To: \n"
|
|
8
|
+
"POT-Creation-Date: 2025-07-25 10:00+0800\n"
|
|
9
|
+
"PO-Revision-Date: 2025-07-25 10:00+0800\n"
|
|
10
|
+
"Last-Translator: rRR0VrFP <rrr0vrfp@qq.com>\n"
|
|
11
|
+
"Language-Team: Chinese (Simplified)\n"
|
|
12
|
+
"Language: zh_Hans\n"
|
|
13
|
+
"MIME-Version: 1.0\n"
|
|
14
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
15
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
16
|
+
"Plural-Forms: nplurals=1; plural=0;\n"
|
|
17
|
+
|
|
18
|
+
# Template strings
|
|
19
|
+
msgid "Key"
|
|
20
|
+
msgstr "键"
|
|
21
|
+
|
|
22
|
+
msgid "Value"
|
|
23
|
+
msgstr "值"
|
|
24
|
+
|
|
25
|
+
msgid "Actions"
|
|
26
|
+
msgstr "操作"
|
|
27
|
+
|
|
28
|
+
msgid "Key name"
|
|
29
|
+
msgstr "键名"
|
|
30
|
+
|
|
31
|
+
msgid "Copy row"
|
|
32
|
+
msgstr "复制行"
|
|
33
|
+
|
|
34
|
+
msgid "Move up"
|
|
35
|
+
msgstr "上移"
|
|
36
|
+
|
|
37
|
+
msgid "Move down"
|
|
38
|
+
msgstr "下移"
|
|
39
|
+
|
|
40
|
+
msgid "Delete"
|
|
41
|
+
msgstr "删除"
|
|
42
|
+
|
|
43
|
+
msgid "+ Add"
|
|
44
|
+
msgstr "+ 添加"
|
|
45
|
+
|
|
46
|
+
msgid "Import"
|
|
47
|
+
msgstr "导入"
|
|
48
|
+
|
|
49
|
+
msgid "Export"
|
|
50
|
+
msgstr "导出"
|
|
51
|
+
|
|
52
|
+
msgid "Paste YAML / JSON / KEY=VALUE format text..."
|
|
53
|
+
msgstr "粘贴 YAML/JSON/KEY=VALUE 格式文本..."
|
|
54
|
+
|
|
55
|
+
msgid "Cancel"
|
|
56
|
+
msgstr "取消"
|
|
57
|
+
|
|
58
|
+
msgid "OK"
|
|
59
|
+
msgstr "确定"
|
|
60
|
+
|
|
61
|
+
msgid "Close"
|
|
62
|
+
msgstr "关闭"
|
|
63
|
+
|
|
64
|
+
# Validation messages
|
|
65
|
+
msgid "Required fields: %s"
|
|
66
|
+
msgstr "请填写必填项:%s"
|
|
67
|
+
|
|
68
|
+
msgid "'%s' must be %s, got '%s'"
|
|
69
|
+
msgstr "'%s' 需要 %s 类型,当前值 '%s' 无效"
|
|
70
|
+
|
|
71
|
+
# Type labels
|
|
72
|
+
msgid "string"
|
|
73
|
+
msgstr "字符串"
|
|
74
|
+
|
|
75
|
+
msgid "text"
|
|
76
|
+
msgstr "多行文本"
|
|
77
|
+
|
|
78
|
+
msgid "number"
|
|
79
|
+
msgstr "浮点数"
|
|
80
|
+
|
|
81
|
+
msgid "integer"
|
|
82
|
+
msgstr "整数"
|
|
83
|
+
|
|
84
|
+
msgid "boolean"
|
|
85
|
+
msgstr "布尔值"
|
|
86
|
+
|
|
87
|
+
msgid "JSON"
|
|
88
|
+
msgstr "JSON"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
.django-kv-config-widget { max-width: 800px; border: 1px solid var(--border-color, #ccc); padding: 0; border-radius: 4px; }
|
|
2
|
+
.django-kv-config-widget:focus-within { outline: 2px solid -webkit-focus-ring-color; outline-offset: 1px; }
|
|
3
|
+
.django-kv-config-widget .django-kv-config-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
4
|
+
.django-kv-config-widget .django-kv-config-table th { text-align: left; padding: 4px 8px; font-weight: 600; }
|
|
5
|
+
|
|
6
|
+
.django-kv-config-widget .django-kv-config-key-col { width: 30%; }
|
|
7
|
+
.django-kv-config-widget .django-kv-config-value-col { width: auto; }
|
|
8
|
+
.django-kv-config-widget .django-kv-config-action-col { width: 20px; }
|
|
9
|
+
.django-kv-config-widget .django-kv-config-table th.django-kv-config-action-col,
|
|
10
|
+
.django-kv-config-widget .django-kv-config-table td.django-kv-config-action-col { text-align: center; white-space: nowrap; overflow: hidden; }
|
|
11
|
+
.django-kv-config-widget .django-kv-config-key-input { width: 100%; box-sizing: border-box; }
|
|
12
|
+
.django-kv-config-widget .django-kv-config-value-input { width: 100%; box-sizing: border-box; padding: 4px 6px; }
|
|
13
|
+
.django-kv-config-widget .django-kv-config-value-input { resize: none; overflow: hidden; max-height: 10em; font-family: inherit; font-size: inherit; line-height: 1.4; }
|
|
14
|
+
.django-kv-config-widget .django-kv-config-value-input[type="number"] { height: 1.875rem; }
|
|
15
|
+
.django-kv-config-widget .django-kv-config-value-input[type="number"] { text-align: right; }
|
|
16
|
+
|
|
17
|
+
.django-kv-config-widget .django-kv-config-table td.django-kv-config-action-col { padding: 2px; vertical-align: middle; }
|
|
18
|
+
|
|
19
|
+
.django-kv-config-widget .django-kv-config-remove {
|
|
20
|
+
cursor: pointer; border: none; background: none; font-size: 14px; padding: 1px 2px; border-radius: 2px; line-height: 1; color: var(--error-fg, #ba2121); transition: background 0.15s; display: block; margin: 0 auto; width: 16px; text-align: center;
|
|
21
|
+
}
|
|
22
|
+
.django-kv-config-widget .django-kv-config-remove:hover { background: rgba(186,33,33,0.1); }
|
|
23
|
+
|
|
24
|
+
.django-kv-config-widget .django-kv-config-toolbar { padding: 4px 8px; display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
25
|
+
.django-kv-config-widget .django-kv-config-add,
|
|
26
|
+
.django-kv-config-widget .django-kv-config-import,
|
|
27
|
+
.django-kv-config-widget .django-kv-config-export {
|
|
28
|
+
cursor: pointer; border: 1px solid var(--border-color, #ccc); background: var(--darkened-bg, #f8f8f8);
|
|
29
|
+
padding: 4px 10px; border-radius: 3px; font-size: inherit;
|
|
30
|
+
}
|
|
31
|
+
.django-kv-config-widget .django-kv-config-add:hover,
|
|
32
|
+
.django-kv-config-widget .django-kv-config-import:hover,
|
|
33
|
+
.django-kv-config-widget .django-kv-config-export:hover { background: #e8e8e8; }
|
|
34
|
+
|
|
35
|
+
.django-kv-config-widget .django-kv-config-required-row .django-kv-config-key-input { background: var(--darkened-bg, #f8f8f8); cursor: default; }
|
|
36
|
+
.django-kv-config-widget .django-kv-config-required-row .django-kv-config-value-input { border-left: 2px solid var(--primary, #79aec8); }
|
|
37
|
+
|
|
38
|
+
.errors .django-kv-config-widget input,
|
|
39
|
+
.errors .django-kv-config-widget textarea,
|
|
40
|
+
.errors .django-kv-config-widget select { border-color: var(--border-color, #ccc); }
|
|
41
|
+
.errors .django-kv-config-widget .django-kv-config-required-row .django-kv-config-value-input[aria-required="true"]:placeholder-shown { border-color: var(--error-fg, #ba2121); }
|
|
42
|
+
.errors .django-kv-config-required-row .django-kv-config-value-input[aria-required="true"]:placeholder-shown { border-left-color: var(--error-fg, #ba2121); }
|
|
43
|
+
|
|
44
|
+
.django-kv-config-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; }
|
|
45
|
+
.django-kv-config-modal-backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); }
|
|
46
|
+
.django-kv-config-modal-dialog { position: relative; background: var(--body-bg, #fff); border: 1px solid var(--border-color, #ccc); border-radius: 4px; padding: 16px; min-width: 400px; max-width: 600px; width: 90%; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
|
47
|
+
.django-kv-config-modal-text { width: 100%; min-height: 200px; max-height: 400px; box-sizing: border-box; font-family: monospace; font-size: 13px; padding: 8px; border: 1px solid var(--border-color, #ccc); border-radius: 3px; }
|
|
48
|
+
.django-kv-config-modal-actions { text-align: right; margin-top: 8px; }
|
|
49
|
+
.django-kv-config-modal-actions button { margin-left: 6px; cursor: pointer; border: 1px solid var(--border-color, #ccc); background: var(--darkened-bg, #f8f8f8); padding: 4px 12px; border-radius: 3px; font-size: inherit; }
|
|
50
|
+
.django-kv-config-modal-actions button:hover { background: #e8e8e8; }
|