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:
- First, I wanted to show how django-dbsettings works, so that those of you who need to add your type of settings should not spend an extra day reading the code for this module;
- secondly, I wanted to convey my wish to the readers (rather, even for “code writers”): do not stop when something works! Continue to revise and correct your code until there is a sense of pride and aesthetic satisfaction for your “child” (but do not make fanaticism: “keep it simple” ©);
- This article is an example of making changes to an open source project and, specifically, how your ideas should fit beautifully in style and logic into a project and not disrupt the work of other modules of the system. My decision was not immediately the way it was presented here: first, other files were affected; there were a few extra ifs and inheritances, etc., but, following the previous paragraph, we managed to minimize and localize the code.
Project structure
(Bold highlighted files in which you had to make changes to implement ImageValue)
- templates / - contains two templates: to view and edit the settings of the entire site and the settings of a separate application
- tests / - contains tests
- __init__.py
- dbsettings.txt - help on the use of the module (here it is described in detail how to use it)
- forms.py - form designer for editing settings
- group.py - defines the group of settings group, controls access rights
- loading.py - contains functions for working with the database (add, save, read settings)
- models.py - contains its model for storing settings
- urls.py - contains urls for viewing and editing settings
- utils.py - contains the function of setting default-values ​​when running syncdb
- values.py - contains description of setting types
- views.py - contains description of view / edit views
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':
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')
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.comThanks 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.