表单数据和json数据的区别_Djangosuitadmin将json格式的数
据拆分成表单。。。
需求背景
有时候为了配置的灵活性、应⽤未来的需求变化、控制单张表的字段数,避免字段过多,会把⼀些字段设置是Json格式。像下⾯这样:
json_field.png
这样的好处是,后⾯如果突然需要加多⼀个字段,就可以直接在加到这个json⾥⾯,既不⽤修改数据表,也不⽤修改程序,只要通知前端我在json⾥⾯加了⼀个字段就好了,对于未来字段设置不确定或者随时改变的情况是⾮常⽅便。
但有⼀个不好的地⽅就是,配置起来很⿇烦,后台可能是给到⼀些不懂技术的⼈⽤,他们看到这么个东
西就很⼀脸蒙蔽,甚⾄有时候技术⾃⼰可能出配着配着⼀不⼩⼼少了个引号,少个了逗号导致json格式错误,这就会导致程序解析json错误然后出现异常,影响了线上应⽤。所以这样的后台对于懂技术不懂技术的⼈来说都是⼀个挑战。
所以我就想着把这个json,拆解成⼀个表单的形式,像下⾯这样:
form.png
这样看起来是不是就直观很多,不管是谁来⽤这个后台都能很轻易的上⼿。
实现原理
原理也很简单。
加载时:
第1步:把 json 解析出来,把 json ⾥的每个字段当做是 model 的单个独⽴的字段去处理,赋值到对象上;
第2步:⾃定义⼀个表单 form , 把这些从 json 解析出来的字段也显⽰到管理后台上。
写⼊时:
第1步:接收⾃定义表单传过来的数据后进来验证(看需求要不要做⼀些数据验证)
第2步:把数据封装成 json 后再赋值到 model 上保存该 json 的字段,然后写⼊数据库保存。
实现思路就这么⼏步,很简单,只是编码过程中会存在⼀些细节的问题,下⾯通过编码来把上⾯的步骤⾛⼀遍。
编码实现
解析 json 并赋值到 model 对象,当成普通字段处理
要处理刚从数据库读出来的数据,只需要重写⼀下 Model 类的⼀个类⽅法from_db
下⾯这⼀段是源码的from_db⽅法
@classmethod
def from_db(cls, db, field_names, values):
if len(values) != len(cls._te_fields):
values_iter = iter(values)
values = [
next(values_iter) if f.attname in field_names else DEFERRED
for f in cls._te_fields
]
new = cls(*values)
new._state.adding = False
new._state.db = db
return new
那我们要做的就是在⾃⼰的 Model 中重写这个⽅法,然后先调⽤⽗类的from_db⽅法完成数据的加载
@classmethod
def from_db(cls, db, field_names, values):
new = super().from_db(db, field_names, values)
# todo 在这⾥添加上 json 解析逻辑
return new
下⾯是我实现的把 json 解析成 model 对象字段的代码,我封装成⼀个类,哪个 model 需要解析 json
的直接继承这个类就好了
class JsonTransToField(models.Model):
@staticmethod
def get_image_name(image_url):
"""
解析图⽚url,去掉url前缀,保留图⽚名称
如:/media/test.png --> test.png
"""
image_url_prefix = f'{MEDIA_DOMAIN}/media/'
image_name = place(image_url_prefix, '')
return image_name
@staticmethod
def dict_to_field(instance, data, prefix):
"""
prefix: 字段名前缀。
核⼼逻辑。把 json 解析到成 model 对象的普通字段。
如:{'test': '123'}  --> st = '123'
"""
for key, value in data.items():
# 递归解析 json ⾥的⼦ json
if isinstance(value, dict):
JsonTransToField.dict_to_field(instance, value, f'{prefix}___{key}')
continue
# 解析图⽚ url 成 ImageField。v.find('alipay-xx.oss') 这段是因为图⽚都是存放在阿⾥去上,⽤来判断该字段是否图⽚字段
if isinstance(value, str) and value.find('alipay-xx.oss') > -1:
setattr(instance, f'{prefix}___{key}', ImageFieldFile(instance, models.ImageField(name=f'{prefix}___{key}'),
<_image_name(value)))
else:
setattr(instance, f'{prefix}___{key}', value)
@classmethod
def from_db(cls, db, field_names, values):
"""
捕获 json 解析异常,避免发⽣异常的时候会影响线上应⽤。
但管理后台该表单会没有数据,因为异常后没有把 json ⾥的数据解析到 instance上
"""
new = super().from_db(db, field_names, values)
try:
# 迭代instance的字段,如果数据是以 { 开头的说明是 json,进⾏解析操作
fields = new.__dict__.copy()
for field, value in fields.items():
if isinstance(value, str) and value.startswith('{'):
data = json.loads(value)
JsonTransToField.dict_to_field(new, data, field)
except Exception:
<("解析life json异常", traceback.format_exc())
return new
class Meta:
abstract = True
有 2 个地⽅说明⼀下:
1. 代码中的{prefix}_{key}是设置到 instance 上的字段名,prefix 指的是 json 字段的字段名,后⾯接 3个下划线(因为2个下划线是外键
的读取⽅法,避免冲突),然后接上 json 是的 key。如:params={"test":"123"} --> params___test
2. setattr(instance, f'{prefix}{key}', ImageFieldFile(instance, models.ImageField(name=f'{prefix}{key}'),
<_image_name(value))) 这段代码是为了把图⽚解析成 ImageFieldFile 类型,这
样图⽚在后台显⽰的样式就是上图⼴告ICON的样⼦,这样⽅便图⽚的上传设置,否则会以图⽚链接的形式显⽰在后台。
⾃定义 form 表单
上⾯我们已经把 json 解析成对象的普通字段了,现在要做的就是把这些字段像 model 定义好的字段⼀样显⽰在后台。
这⾥先假设数据表⾥有⼀个这样的 json 字段,⽅便理解:
params = {"task": "", "reward": "", "adv": {"icon": "", "title": "", "subtitle": ""}, "link": "", "link_type": "TO_APPLET_PAGE", "app_id": "", "path": ""}
django admin 自定义页面我们定义⼀个 form 如下:
class CustomForm(ModelForm):
params___task = CharField(label='任务内容', max_length=20, required=False)
params___reward = CharField(label='任务奖励说明', max_length=20, required=False)
params___adv___icon = ImageField(label='⼴告ICON', required=False)
params___adv___title = CharField(label='任务标题', max_length=20, required=False)
params___adv___subtitle = CharField(label='任务副标题', max_length=20, required=False)
params___link = CharField(label='链接', max_length=150, required=False)
params___link_type = TypedChoiceField(label='链接类型', choices=[('TO_APPLET_PAGE', '⼩程序'), ('TO_H5', 'H5'), ('TO_APPLET_LOCAL_PAGE', '本地页⾯'    params___app_id = CharField(label='appId', required=False)
params___path = CharField(label='path', required=False)
# 把 model 定义的字段定义也加上
# ················
# 重写__init__⽅法。初始化 form 的时候,把 instance 中解析出来的 json 字段添加到 form 的 initial 中,否则后台不会显⽰出来
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
empty_permitted=False, instance=None, use_required_attribute=None,
renderer=None):
super(CustomForm, self).__init__(data, files, auto_id, prefix,
initial, error_class, label_suffix,
empty_permitted, instance, use_required_attribute,
renderer)
if instance is not None:
for k, field in instance.__dict__.items():
if k.find('___') > - 1:
self.initial[k] = getattr(instance, k)
def save_image(self, instance, file, name):
"""
封装成ImageFieldFile,并保存上传的图⽚资源
"""
# 没有上传图⽚是 'None'
if str(file) == 'None':
return ''
image = models.ImageField(upload_to='you store folder/', name=name)
image_file = ImageFieldFile(instance, image, str(file))
image_file._file = file
# 发⽣更改的图⽚是 InMemoryUploadedFile 类型,这种情况才需要保存图⽚资源
if isinstance(file, InMemoryUploadedFile):
image_file.save(image_file.name, image_file.file, save=False)
return image_file
# 核⼼逻辑。提交表单,把⾃定义表单字段组装成json
def clean(self):
# data: 存放 dict 数据
data = {}
for key, value in self.fields.items():
if key.find('___') > - 1:
# 这⾥⼀个 for 循环是为了递归的封装 dict.
# 如果 params___adv___icon、params___title  --> {"params": {"title": ""}, "adv": {"icon": ""}}
parents = key.split('___')
# d: 当前进⾏封装的 dict
d = {}
p = data
for parent in parents[:-1]:
d = p.setdefault(parent, {})
p = d
# 图⽚资源则保存图⽚或上传到云存储,然后包装成完整的访问 url
# parent[-1] 就是是⾥⾯⼀层的字段名。如:params___adv___icon --> ['params', 'adv', 'icon']
if isinstance(value, ImageField):
image_file = self.save_image(self.instance, self.(key), key)
if image_file:
image_file = f'{MEDIA_DOMAIN}/media/{image_file.name}'
d[parents[-1]] = image_file
else:
d[parents[-1]] = self.(key, '')
# 最后把封装好的 dict 转成 json 赋值到对应的字段
for k, v in data.items():
setattr(self.instance, k, json.dumps(v, ensure_ascii=False))
class Meta:
model = You Model
fields = '__all__'
这⾥主要是重写了 ModelForm 的 clean ⽅法,在⾥⾯将特定数据封装成 json 然后再保存到 model 中。代码功能都带注释了。
clean ⾥⾯的逻辑最好是⾃⼰跟着实现⼀遍,调试⼀下,直观的看封装过程会更容易理解,单看代码可能会有点难理解。
替换⾃带 form
最后⼀步,把上⾯写好的 form , 添加到admin中,还可以加⼀个tab,把⾃定义的表单单独出来⼀个 tab ,避免很多字段揉杂在⼀起显得乱。
class CustomAdmin(admin.ModelAdmin):
# ············
form = CustomForm
fieldsets = [
(None, {
'classes': ('suit-tab', 'suit-tab-general'),
'fields': []  # 这⾥放基础的字段
}),
('跳转链接配置', {
'classes': ('suit-tab', 'suit-tab-link'),
'fields': ['params___task', 'params___reward', 'params___adv___icon', 'params___adv___title',
'params___adv___subtitle', 'params___link', 'params___link_type', 'params___app_id', 'params___path']  # 这⾥放⾃定义表单的字段        })]
suit_form_tabs = [('general', '基础'), ('link', '跳转链接配置')]
# ············
最后效果图如下:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。