Добрый день.
Часто возникает необходимость иметь пользовательские (административные) настройки сайта, которые не могут быть определены в settings.py по двум простым причинам: настройки из settings.py не могут быть изменены без перезапуска сервера; и — самое главное — они могут быть изменены только программистом.
Модуль django-dbsettings (бывш. django-values) призван избавить Вас от этих ограничений: он предоставляет механизм хранения пользовательских настроек в базе данных, а также удобные виды для их редактирования.
И вроде бы все отлично… НО! Что же делать, если в качестве настройки нужна будет картинка: например, логотип сайт��? Как выяснилось, django-dbsettings не поддерживает такого типа значений.
О том, как я добавлял поддержку ImageValue в django-dbsettings, я и собираюсь поведать.
Когда передо мной встала задача сделать настройки, я нашел проект django-values, который оказался нерабочим. Помучившись с ним, узнал, что он был переименован в django-dbsettings и перемещен на github.
На githab'е обнаружилось 20 форков. Перепробовав несколько из них, остановился на том, который обновлялся последним. Он оказался рабочим и завелся с первого раза без магии. Единственной проблемой осталось отсутствие типа «Картинка» в настройках.
Было два варианта: либо городить костыли у себя в проекте, чтобы добавить картинки в качестве настроек, либо форкнуть проект и сделать все красиво. Выбор, был очевиден.
Целей написания этой статьи было несколько:
(Жирным выделены файлы, в которые нужно было внести изменения для внедрения ImageValue)
Покажу внесённые изменения в patch-виде: "-" — удаленная строка, "+" — добавленная строка.
Описание формы в шаблонах django-dbsettings не содержало enctype, необходимого для того, чтобы форма принимала файлы, а вид получал их в request.FILES.
Чтобы загруженные файлы проходили валидацию и попадали в form.cleaned_data с другими введёнными данными, нужно при создании формы передавать ей принятые файлы из запроса.
На этом остановимся по-подробней.
Файл values.py содержит описание базового класса для настроек. В нем помимо всего прочего есть три метода, которые должны быть переопределены во всех дочерних классах:
Также класс Value должен иметь атрибут field, в котором должен храниться класс поля формы (напр. django.forms.FileInput) для его создания.
Наследуемся от базового класса Value, обрабатываем ��вой параметр upload_to, чтобы можно было контролировать подпапку в IMAGE_ROOT, в которую будут заливаться пользовательские изображения.
Переопределяем методы, отвечающие за отображение значения на разных этапах использования _настройки_.
Начнем с загрузки картинки и сохранения её в базе.
Параметр value содержит объект UploadedFile из django.core.files.uploadedfile. Это стандартный объект, создаваемый при загрузке файлов и попадающий в request.FILES.
Метод производит нехитрые махинации: создает уникальное имя файла, и копирует загруженный файл в нужную директорию, указанную в self._upload_to. Метод возвращает путь до картинки относительно settings.IMAGE_ROOT, в таком виде настройка и попадает в базу данных.
Теперь сделаем обратное преобразование: получим объект картинки из записи в базе данных, за это отвечает следующий метод:
Тут все делается в обратном порядке: составляем путь до картинки со знач��нием, взятым из базы, создаем объект SimpleUploadedFile и читаем в него файл картинки.
Объясню, зачем нужна строчка:
Дело в том, что базовый класс для загруженных файлов UploadedFile имеет setter для атрибута name, который от переданного пути отрезает только имя файла и сохраняет его в self._name, а getter возвращает это значение. Записать туда руками путь до картинки — самый быстрый способ передачи его в свой виджет для формы.
И остался только метод, возвращающий объект для сравнения. Этот объект нужен при сравнении значения, полученного из запроса, с текущим значением из базы, чтобы лишний раз не перезаписывать файл. Тут все просто:
Остался последний штрих: свой виджет, который рядом со стандартной кнопкой заливки файла будет отображать текущую картинку из базы.
Создаем свой класс field, а в нем — widget, наследованный от стандартного FileInput. Переопределяем метод render, который отвечает за отображение нашего input'а, возвращая соответствующий html.
Эта строчка выполняет сразу две необходимых проверки: существует ли указанный файл, и является ли он изображением, в обоих случаях может выбросить исключение IOError.
Функция mark_safe() помечает строку безопасной для вывода html (без этого код нашего виджета просто выведется в виде строчки на странице).
Конечный результат выглядит таким образом:

django-dbsettings на github.com
Собираюсь и дальше поддерживать этот проект, поэтому было бы здорово, если люди, опробовавшие его в деле, выразят свои пожелания, либо пожалуются на баги.
Часто возникает необходимость иметь пользовательские (административные) настройки сайта, которые не могут быть определены в settings.py по двум простым причинам: настройки из settings.py не могут быть изменены без перезапуска сервера; и — самое главное — они могут быть изменены только программистом.
Модуль django-dbsettings (бывш. django-values) призван избавить Вас от этих ограничений: он предоставляет механизм хранения пользовательских настроек в базе данных, а также удобные виды для их редактирования.
И вроде бы все отлично… НО! Что же делать, если в качестве настройки нужна будет картинка: например, логотип сайт��? Как выяснилось, django-dbsettings не поддерживает такого типа значений.
О том, как я добавлял поддержку ImageValue в django-dbsettings, я и собираюсь поведать.
Предыстория
Когда передо мной встала задача сделать настройки, я нашел проект django-values, который оказался нерабочим. Помучившись с ним, узнал, что он был переименован в django-dbsettings и перемещен на github.
На githab'е обнаружилось 20 форков. Перепробовав несколько из них, остановился на том, который обновлялся последним. Он оказался рабочим и завелся с первого раза без магии. Единственной проблемой осталось отсутствие типа «Картинка» в настройках.
Было два варианта: либо городить костыли у себя в проекте, чтобы добавить картинки в качестве настроек, либо форкнуть проект и сделать все красиво. Выбор, был очевиден.
Цели
Целей написания этой статьи было несколько:
- во-первых, я хотел показать, как функционирует django-dbsettings, чтобы те из вас, кому понадобится добавить свой тип _настройки_, не тратили лишний день на чтение кода этого модуля;
- во-вторых, мне хотелось донести до читателей (скорей даже для «писателей кода») своё пожелание: не останавливайтесь, когда что-то заработает! Продолжайте пересматривать и исправлять свой код, пока не возникнет чувство гордости и эстетического удовлетворения за своё «детище» (но не доводите до фанатизма: «keep it simple» © );
- статья является примером внесения изменений в open source проект и, конкретно, того, как Ваши наработки должны красиво вписываться по стилю и логике в проект и не нарушать работы других модулей системы. Моё решение не сразу было таким, как оно представлено тут: сначала были затронуты и другие файлы; было несколько лишних if'ов и наследований, и т.д., но, следуя предыдущему пункту, удалось минимизировать и локализовать код.
Структура проекта
(Жирным выделены файлы, в которые нужно было внести изменения для внедрения ImageValue)
- templates/ — содержит два шаблона: для просмотра и редактирования настроек всего сайта и настроек отдельного приложения
- tests/ — содержит тесты
- __init__.py
- dbsettings.txt — справка по использованию модуля (тут подробно описано, как им пользоваться)
- forms.py — конструктор формы для редактирования настроек
- group.py — определяет класс группы настроек, управляет правами доступа
- loading.py — содержит функции работы с базой (добавить, сохранить, прочитать настройки)
- models.py — содержит свою модель для хранения настроек
- urls.py — содержит url'ы для страниц просмотра и редактирования настроек
- utils.py — содержит функцию выставления default-значений при выполнении syncdb
- values.py — содержит описание типов настроек
- views.py — содержит описание видов просмотра/редактирования
Чего же нам не хватает?
Покажу внесённые изменения в 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) для его создания.
Пишем свой 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) ...
Наследуемся от базового класса 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
Спасибо за внимание.
PS
Собираюсь и дальше поддерживать этот проект, поэтому было бы здорово, если люди, опробовавшие его в деле, выразят свои пожелания, либо пожалуются на баги.
