Demand background
Some fields are sometimes set in Json format for flexibility in configuration, to accommodate future changes in requirements, to control the number of fields in a single table, and to avoid excessive fields. Something like this:
The advantage of this is that if you suddenly need to add a field later, you can directly add it to the JSON without modifying the table or the program. You just need to notify the front end that I have added a field in the JSON, which is very convenient for the future field Settings are uncertain or changing at any time.
But there is a bad place is a great trouble to the configuration, the background may be for some people who don’t understand the technology to use, they see such a thing is a face is veiled, sometimes even technology may themselves out with with a less easy to quotes, less a comma in the json format error, it will cause the program parsing json error and abnormal, It affects online applications. So this kind of background is a challenge for people who understand technology and don’t understand technology.
So I thought I’d disassemble this JSON into a form like this:
This looks a lot more intuitive, no matter who to use this background can be very easy to use.
Realize the principle of
The principle is simple.
Loading: Step 1: parse out the JSON, treat each field in the JSON as a single independent field of model to process, assign value to the object; Step 2: Customize a form that displays the fields parsed from JSON in the admin backend.
When writing: Step 1: Verify the data sent from the custom form after receiving it (see whether some data verification is needed) Step 2: encapsulate the data into JSON and then assign the value to the field storing the JSON on the model, and then write the data into the database for saving.
The realization of the idea is so few steps, very simple, but there will be some details in the process of coding, the following steps through the coding to go again.
coded
Parse the JSON and assign to the Model object, treating it as a normal field
To process data that has just been read from the database, you simply override a class method from_db of the Model class
Below is the from_DB method of the source code
@classmethod
def from_db(cls, db, field_names, values) :
if len(values) ! =len(cls._meta.concrete_fields):
values_iter = iter(values)
values = [
next(values_iter) if f.attname in field_names else DEFERRED
for f in cls._meta.concrete_fields
]
new = cls(*values)
new._state.adding = False
new._state.db = db
return new
Copy the code
All we need to do is override this method in our Model and call the from_db method of the parent class to load the data
@classmethod
def from_db(cls, db, field_names, values) :
new = super().from_db(db, field_names, values)
# Todo adds json parsing logic here
return new
Copy the code
Here is the code I implemented to parse JSON into a Model object field. I wrapped it into a class, which model needs to parse JSON directly inherits this class
class JsonTransToField(models.Model) :
@staticmethod
def get_image_name(image_url) :
"" "resolution image url, remove the url prefix, keep the image name Such as: http://test.xxx.com/media/test.png - > test. PNG "" "
image_url_prefix = f'{MEDIA_DOMAIN}/media/'
image_name = image_url.replace(image_url_prefix, ' ')
return image_name
@staticmethod
def dict_to_field(instance, data, prefix) :
""" prefix: prefix of the field name. Core logic. Parse the JSON into the normal fields of the Model object. {'test': '123'} --> instance.test = '123' ""
for key, value in data.items():
Parse the child JSON within json recursively
if isinstance(value, dict):
JsonTransToField.dict_to_field(instance, value, f'{prefix}___{key}')
continue
# Parse the image URL into ImageField. V. form (' Alipay -xx.oss') because the pictures are stored on Ali, it is used to determine whether the field is a picture field
if isinstance(value, str) and value.find('alipay-xx.oss') > -1:
setattr(instance, f'{prefix}___{key}', ImageFieldFile(instance, models.ImageField(name=f'{prefix}___{key}'),
JsonTransToField.get_image_name(value)))
else:
setattr(instance, f'{prefix}___{key}', value)
@classmethod
def from_db(cls, db, field_names, values) :
""" Catch JSON parsing exceptions to avoid exceptions that may affect online applications. However, there will be no data in this form in the management background, because the data in JSON is not parsed to instance after the exception.
new = super().from_db(db, field_names, values)
try:
Iterate over the instance field, if the data starts with {it is JSON, parse it
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:
LogUtil.error("Parse life JSON exception", traceback.format_exc())
return new
class Meta:
abstract = True
Copy the code
There are two points to illustrate:
- In the code
{prefix}___{key}
Prefix is the name of the field set to instance, prefix is the name of the JSON field, followed by three underscores (because the two underscores are read methods for foreign keys to avoid collisions), and then followed by the JSON key. For example, params={“test”:”123″} –> params___test setattr(instance, f'{prefix}___{key}', ImageFieldFile(instance, models.ImageField(name=f'{prefix}___{key}'), JsonTransToField.get_image_name(value)))
This code parses the image to the ImageFieldFile type, so that the image will look like the image above in the backgroundAdvertising ICON
In order to facilitate image uploading Settings, otherwise it will be shown in the background in the form of image links.
Customize the form form
Now that we’ve parsed the JSON into the normal fields of the object, all we need to do is display these fields in the background as defined by the Model. Let’s assume that the table has a JSON field like this:
params = {"task": ""."reward": ""."adv": {"icon": ""."title": ""."subtitle": ""}, "link": ""."link_type": "TO_APPLET_PAGE"."app_id": ""."path": ""}
Copy the code
We define a form as follows:
class CustomForm(ModelForm) :
params___task = CharField(label='Task content', max_length=20, required=False)
params___reward = CharField(label='Mission Reward Description', max_length=20, required=False)
params___adv___icon = ImageField(label='advertising ICON', required=False)
params___adv___title = CharField(label='Task Title', max_length=20, required=False)
params___adv___subtitle = CharField(label='Task subtitle', max_length=20, required=False)
params___link = CharField(label='link', max_length=150, required=False)
params___link_type = TypedChoiceField(label='Link type', choices=[('TO_APPLET_PAGE'.'applet'), ('TO_H5'.'H5'), ('TO_APPLET_LOCAL_PAGE'.'Local page')], required=False)
params___app_id = CharField(label='appId', required=False)
params___path = CharField(label='path', required=False)
# Override __init__ method. When initializing the form, add the json field parsed from instance to initial, otherwise it won't show up in the background
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) :
""" Encapsulate an ImageFieldFile and save the uploaded image resources """
# no uploaded image is '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
The changed image is of type InMemoryUploadedFile, in which case the image resource needs to be saved
if isinstance(file, InMemoryUploadedFile):
image_file.save(image_file.name, image_file.file, save=False)
return image_file
# Core logic. Submit the form and assemble the custom form fields into JSON
def clean(self) :
# data: Stores dict data
data = {}
for key, value in self.fields.items():
if key.find('_') > - 1:
# Here a for loop is intended to recursively encapsulate dict.
# if params___adv___icon, params___title - > {" params ": {" title" : ""}," adv ": {" icon" : ""}}
parents = key.split('_')
# d: The currently encapsulated dict
d = {}
p = data
for parent in parents[:-1]:
d = p.setdefault(parent, {})
p = d
Image resources save images or upload them to cloud storage and wrap them into full access urls
# parent[-1] is the name of the field in the inner layer. Such as: params___adv___icon - > [' params', 'adv', 'icon']
if isinstance(value, ImageField):
image_file = self.save_image(self.instance, self.cleaned_data.get(key), key)
if image_file:
image_file = f'{MEDIA_DOMAIN}/media/{image_file.name}'
d[parents[-1]] = image_file
else:
d[parents[-1]] = self.cleaned_data.get(key, ' ')
Finally, convert the packaged dict into JSON and assign values to the corresponding fields
for k, v in data.items():
setattr(self.instance, k, json.dumps(v, ensure_ascii=False))
class Meta:
model = You Model
fields = '__all__'
Copy the code
The main thing here is to rewrite the Clean method of ModelForm to encapsulate specific data as JSON and store it in the Model. The code functions are commented out. The logic in Clean is best implemented by yourself, debugging, and watching the packaging process is easier to understand, just looking at the code may be a little difficult to understand.
Replace the native Form
The last step is to add the form to admin. You can also add a TAB to separate the custom form from a TAB, so that many fields are not cluttered together.
class CustomAdmin(admin.ModelAdmin) :
# · · · · · · · · · · · ·
form = CustomForm
fieldsets = [
(None, {
'classes': ('suit-tab'.'suit-tab-general'),
'fields': [] # Put the basic field here}),'Jump link Configuration', {
'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'] # put the fields of the custom form here
})]
suit_form_tabs = [('general'.'base'), ('link'.'Jump link Configuration')]
# · · · · · · · · · · · ·
Copy the code
The final effect is as follows:
You can also set multiple tabs in the header, append them to the fieldSets list. This allows you to rearrange fields to your liking or logic.
At this point, our requirements are complete, and the form form is more user-friendly and easier to use than the ugly, hard to configure, and error-prone json string configuration we started with.