📜 ⬆️ ⬇️

ImageValue in django-dbsettings

Good day.

It often becomes necessary to have user (administrative) site settings that cannot be defined in settings.py for two simple reasons: the settings from settings.py cannot be changed without restarting the server; and - most importantly - they can only be changed by a programmer.

The django-dbsettings module (formerly django-values) is designed to relieve you of these limitations: it provides a mechanism for storing user settings in the database, as well as convenient views for editing them.
')
And everything seems to be fine ... BUT! What to do if you need a picture as a setting: for example, a site logo? As it turned out, django-dbsettings does not support this type of value.

About how I added ImageValue support in django-dbsettings, I'm going to tell.


Prehistory


When I faced the task of making the settings, I found the django-values ​​project, which turned out to be inactive. Having suffered with him, I learned that he was renamed django-dbsettings and moved to github.
There were 20 forks on githab. Having tried several of them, I stopped at the one that was updated last. He turned out to be a worker and started up the first time without magic. The only problem left is the lack of the “Picture” type in the settings.

There were two options: either to fix crutches in my project, to add pictures as settings, or to fork a project and make everything beautiful. The choice was obvious.

Goals


There were several goals for writing this article:



Project structure


(Bold highlighted files in which you had to make changes to implement ImageValue)




What are we missing?


I will show the changes in the patch-form: " - " - deleted line, " + " - added line.

Templates

-<form method="post"> +<form enctype="multipart/form-data" method="post"> ... </form> 

The form description in the django-dbsettings templates did not contain the enctype necessary for the form to accept files, and the view would receive them in request.FILES .

Kinds

 ... if request.method == 'POST': # Populate the form with user-submitted data - form = editor(request.POST.copy()) + form = editor(request.POST.copy(), request.FILES) ... 

In order for the uploaded files to pass validation and get into form.cleaned_data with other data entered, you must pass the received files from the request when creating the form.

Types of settings

At this dwell in more detail.
The values.py file contains a description of the base class for settings. It among other things, there are three methods that must be redefined in all child classes:
 ... class Value(object): ... def to_python(self, value): """ native-python ,       """ return value def get_db_prep_save(self, value): """  pre-save    ,     CharField   """ return unicode(value) def to_editor(self, value): """     ,      """ return unicode(value) ... 

Also, the Value class must have a field attribute in which the form field class must be stored (eg django.forms.FileInput) to create it.

Writing Your Value

 class ImageValue(Value): def __init__(self, *args, **kwargs): if 'upload_to' in kwargs: self._upload_to = kwargs['upload_to'] del kwargs['upload_to'] super(ImageValue, self).__init__(*args, **kwargs) ... 

Inheriting from the base class Value , we process our upload_to parameter so that we can control the subfolder in IMAGE_ROOT into which custom images will be uploaded.

We override the methods responsible for displaying values ​​at different stages of using _tune_.
Let's start by loading a picture and saving it in the database.
 from os.path import join as pjoin class ImageValue(Value): ... def get_db_prep_save(self, value): if not value: return None hashed_name = md5(unicode(time.time())).hexdigest() + value.name[-4:] image_path = pjoin(self._upload_to, hashed_name) dest_name = pjoin(settings.MEDIA_ROOT, image_path) with open(dest_name, 'wb+') as dest_file: for chunk in value.chunks(): dest_file.write(chunk) return unicode(image_path) ... 

The value parameter contains the UploadedFile object from django.core.files.uploadedfile . This is a standard object created when files are loaded and that falls into request.FILES .
The method produces simple frauds: it creates a unique file name, and copies the downloaded file to the desired directory specified in self._upload_to . The method returns the path to the image relative to settings.IMAGE_ROOT , in this form, the setting and gets into the database.

Now we will do the inverse transformation: we will get the image object from the record in the database, the following method is responsible for this:
 class ImageValue(Value): ... def to_editor(self, value): if not value: return None file_name = pjoin(settings.MEDIA_ROOT, value) try: with open(file_name, 'rb') as f: uploaded_file = SimpleUploadedFile(value, f.read(), 'image') #  ""      name uploaded_file.__dict__['_name'] = value return uploaded_file except IOError: return None ... 

Everything is done in reverse order: composing the path to the image with the value taken from the database, creating the SimpleUploadedFile object and reading the image file into it.
Let me explain why the line is needed:
 uploaded_file.__dict__['_name'] = value 

The point is that the base class for uploaded files UploadedFile has a setter for the name attribute, which cuts off only the file name from the passed path and stores it in self._name , and getter returns this value. Writing there with your hands the way to the image is the fastest way to transfer it to your widget for the form.

And there was only a method that returns an object for comparison. This object is needed when comparing the value obtained from the request with the current value from the database, so that it does not overwrite the file. Everything is simple:
 class ImageValue(Value): ... def to_python(self, value): return unicode(value) ... 


The final touch is left: your own widget, which next to the standard file fill button will display the current image from the base.
 class ImageValue(Value): ... class field(forms.ImageField): class widget(forms.FileInput): "Widget with preview" def __init__(self, attrs={}): forms.FileInput.__init__(self, attrs) def render(self, name, value, attrs=None): output = [] try: if not value: raise IOError('No value') Image.open(value.file) file_name = pjoin(settings.MEDIA_URL, value.name) output.append(u'<p><img src="{}" width="100" /></p>'.format(file_name)) except IOError: pass output.append(forms.FileInput.render(self, name, value, attrs)) return mark_safe(''.join(output)) ... 

We create our own field class, and in it the widget inherited from the standard FileInput . Override the render method, which is responsible for displaying our input, returning the corresponding html.

 Image.open(value.file) 

This line performs two necessary checks at once: whether the specified file exists, and if it is an image, in both cases it can throw an IOError exception.
The function mark_safe () marks the line safe for html output (without this, the code of our widget is simply displayed as a line on the page).

The end result looks like this:



Links


django-dbsettings on github.com

Thanks for attention.




PS


I am going to continue to support this project, so it would be great if the people who tried it out in business express their wishes or complain about bugs.

Source: https://habr.com/ru/post/141218/


All Articles