- PVSM.RU - https://www.pvsm.ru -
Добрый день.
Часто бывает нужно иметь пользовательские (административные) настройки сайта, которые не могут быть определены в settings.py по двум простым причинам: настройки из settings.py не могут быть изменены без перезапуска сервера; и — самое главное — они могут быть изменены только программистом.
Модуль django-dbsettings (бывш. django-values) призван избавить Вас от этих ограничений: он предоставляет механизм хранения пользовательских настроек в базе данных, а также удобные виды для их редактирования.
И вроде бы все отлично… НО! Что же делать, если в качестве настройки нужна будет картинка: например, логотип сайта? Как выяснилось, django-dbsettings не поддерживает такого типа значений.
О том, как я добавлял поддержку ImageValue в django-dbsettings, я и собираюсь поведать.
Когда передо мной встала задача сделать настройки, я нашел проект django-values, который оказался нерабочим. Помучившись с ним, узнал, что он был переименован в django-dbsettings и перемещен на github.
На githab'е обнаружилось 20 форков. Перепробовав несколько из них, остановился на том, который обновлялся последним. Он оказался рабочим и завелся с первого раза без магии. Единственной проблемой осталось отсутствие типа «Картинка» в настройках.
Было два варианта: либо городить костыли у себя в проекте, чтобы добавить картинки в качестве настроек, либо форкнуть проект и сделать все красиво. Выбор, был очевиден.
Целей написания этой статьи было несколько:
Покажу внесённые изменения в patch-виде: "-" — удаленная строка, "+" — добавленная строка.
-<form method="post">
+<form enctype="multipart/form-data" method="post">
...
</form>
Описание формы в шаблонах django-dbsettings не содержало enctype, необходимого для того, чтобы форма принимала файлы, а вид получал их в request.FILES.
...
if request.method == 'POST':
# Populate the form with user-submitted data
- form = editor(request.POST.copy())
+ form = editor(request.POST.copy(), request.FILES)
...
Чтобы загруженные файлы проходили валидацию и попадали в form.cleaned_data с другими введёнными данными, нужно при создании формы передавать ей принятые файлы из запроса.
На этом остановимся по-подробней.
Файл values.py содержит описание базового класса для настроек. В нем помимо всего прочего есть три метода, которые должны быть переопределены во всех дочерних классах:
...
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)
...
Также класс Value должен иметь атрибут field, в котором должен храниться класс поля формы (напр. django.forms.FileInput) для его создания.
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)
...
Наследуемся от базового класса Value, обрабатываем свой параметр upload_to, чтобы можно было контролировать подпапку в IMAGE_ROOT, в которую будут заливаться пользовательские изображения.
Переопределяем методы, отвечающие за отображение значения на разных этапах использования _настройки_.
Начнем с загрузки картинки и сохранения её в базе.
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)
...
Параметр value содержит объект UploadedFile из django.core.files.uploadedfile. Это стандартный объект, создаваемый при загрузке файлов и попадающий в request.FILES.
Метод производит нехитрые махинации: создает уникальное имя файла, и копирует загруженный файл в нужную директорию, указанную в self._upload_to. Метод возвращает путь до картинки относительно settings.IMAGE_ROOT, в таком виде настройка и попадает в базу данных.
Теперь сделаем обратное преобразование: получим объект картинки из записи в базе данных, за это отвечает следующий метод:
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
...
Тут все делается в обратном порядке: составляем путь до картинки со значением, взятым из базы, создаем объект SimpleUploadedFile и читаем в него файл картинки.
Объясню, зачем нужна строчка:
uploaded_file.__dict__['_name'] = value
Дело в том, что базовый класс для загруженных файлов UploadedFile имеет setter для атрибута name, который от переданного пути отрезает только имя файла и сохраняет его в self._name, а getter возвращает это значение. Записать туда руками путь до картинки — самый быстрый способ передачи его в свой виджет для формы.
И остался только метод, возвращающий объект для сравнения. Этот объект нужен при сравнении значения, полученного из запроса, с текущим значением из базы, чтобы лишний раз не перезаписывать файл. Тут все просто:
class ImageValue(Value):
...
def to_python(self, value):
return unicode(value)
...
Остался последний штрих: свой виджет, который рядом со стандартной кнопкой заливки файла будет отображать текущую картинку из базы.
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))
...
Создаем свой класс field, а в нем — widget, наследованный от стандартного FileInput. Переопределяем метод render, который отвечает за отображение нашего input'а, возвращая соответствующий html.
Image.open(value.file)
Эта строчка выполняет сразу две необходимых проверки: существует ли указанный файл, и является ли он изображением, в обоих случаях может выбросить исключение IOError.
Функция mark_safe() помечает строку безопасной для вывода html (без этого код нашего виджета просто выведется в виде строчки на странице).
Конечный результат выглядит таким образом:
django-dbsettings на github.com [1]
Собираюсь и дальше поддерживать этот проект, поэтому было бы здорово, если люди, опробовавшие его в деле, выразят свои пожелания, либо пожалуются на баги.
Автор: hdg700
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/4716
Ссылки в тексте:
[1] django-dbsettings на github.com: https://github.com/hdg700/django-dbsettings
Нажмите здесь для печати.