СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

H Django standalone models в черновиках

imageДобрый день, хабравчане.

Совсем недавно я вдруг открыл для себя всю мощь моделей и форм Django.

Архитектура Django — это Model-View-Template (MVT). Модель отображает данные в базе, вид выполняет код приложения, шаблон занимается выводом. Часть этой тройки — Template, шаблоны — можно использовать отдельно от Django вообще — загружать шаблон и рендерить его с учётом контекста. Об этом говорится в документации.

Однако в документации не говорится, что ещё одна часть — Model — также вполне самостоятельна, и даже более того — её использование вкупе с формами в правильных случаях сильно упрощает жизнь разработчику и делает код простым и понятным.


Итак, поговорим подробнее о моделях.

Модели


Модель — это класс с заданными полями:
class SomeModel(models.Model):
    name = models.CharField('name of SomeModel', blank=True, max_length=255)

Django уже определил для нас всевозможные поля, навроде CharField, PositiveIntegerField, EmailField и так далее, то есть думать не надо — всё уже есть, а чего нет — можно дописать самому. У полей есть атрибуты — максимальная длина поля, возможность пустого значения.

Модели соответствует таблица в базе данных, которая создаётся после manage.py syncdb… или нет? Всем ли моделям соответствует таблица в базе данных? Знающие люди сразу ответят: нет, ведь есть абстрактные базовые классы.

Абстрактные базовые классы


Заглянем в документацию:
Абстрактные базовые классы полезны, когда вы хотите использовать одну и ту же информацию в нескольких моделях. Вы пишите базовый класс и ставите ему abstract=True в его Meta-класс. Эта модель не будет создавать таблицу в базе данных. Вместо этого эта модель будет использована как базовый класс для других моделей, её поля будут добавлены к полям родительского класса.

К сожалению, это единственное применение абстрактных моделей, поэтому отбросим этот вариант как скучный:
Абстрактная модель не может использоваться как нормальная модель Django. Она не генерирует таблицу в базе данных, у неё нет менеджера, и на её основе нельзя создавать инстансы и сохранять их. (вольный перевод)

И всё же, всем ли моделям соответствует таблица в базе данных?

Standalone-модели


В своё время, в связи с ростом количества классов в models.py, я хотел сделать разбиение этого файла на несколько логически обособленных кусочков. Задача решаема, но в ходе разбиения я заметил одну интересную вещь: если определить модель где-нибудь вне models.py (вне видимости стандартного парсера моделей django), то при manage.py syncdb таблица не создаётся, однако модель… работает! То есть это абсолютно валидная конструкция, которую можно создавать в памяти, заполнять данными, вызывать встроенные функции, ну и вообще вытворять всё, что мы привыкли проделывать с моделями django — за исключением сохранения, удаления и прочих операций, связанных с базой данных (соответствующая таблица попросту не существует).

Внешне — никаких отличий от обычных моделей, за исключением расположения:
# project/app/StandaloneModels/models.py
# Если бы модель была в project/app/models.py - то таблица создавалась бы
class SomeModel(models.Model):
    name = models.CharField('name of SomeModel', blank=True, max_length=255)  # Задавать null бессмысленно


Я их называю standalone models — что-то вроде «одинокие модели» — потому что они находятся не с основными моделями и ведут себя иначе.

Ну и зачем?


Логичный вопрос. Что нам дают такие модели? А вот что:
  • Они как простые классы в python, но с функционалом от django — валидация email, проверка длины полей, проверка на «пустоту» значения, преобразование в dict, что угодно.
  • Интеграция с мощным инструментом — формами от django.


Примеры


Приведу пример из моей недавней практики.

Есть внешний модуль для заполнения бланков «Почты России» и вывода pdf. Модуль — это класс вроде
class RusPost():
    def __init__(
        self,
        client={
            'name': '',
            'address': '',
            'index': '',
        }
    ):
        self.client = client

    def render(self):
        # ... some magic code
        return render_pdf(self.client)

При инициализации инстанса класса мы передаём в словаре данные клиента (client), для генерации pdf вызываем render().

Появилась задача создать html-форму (инерфейс пользователя) для взаимодействия с библиотекой:
image
Какие тут есть варианты решения задачи?

1. Хардкор-стайл для отлучённых от Django

Вручную писать html форму, которая передаёт данные в view, который вызывает библиотеку:
<!-- template.html -->
<form action="" method="POST">
    <input type="text" name="name">
    ...
    <input type="submit" value="Сгенерировать форму">
</form>

def toPdf(request):
    ruspost = RusPost(
        client={
            'name': request.POST.get('name', ''),
            'address': request.POST.get('address', ''),
            'index': request.POST.get('index', ''),
        }
    )
    ruspost.render()
    ...

Минусы — ручной мартышкин труд.

2. Form-стайл

Создать форму
# forms.py
class RuspostClientForm(forms.Form):
    name = forms.CharField()
    address = forms.CharField()
    index = forms.CharField()

<!-- template.html -->
<form action="" method="POST">
    {{ form }}
    <input type="submit" value="Сгенерировать форму">
</form>

def toPdf(request):
    form = RuspostForm(request.POST or None)
    if form.is_valid():
        ruspost = RusPost(
            client=form.cleaned_data,
        )
        ruspost.render()
    ...

Чем хороши формы — это
  • автогенерация формы (использование {{ form }})
  • валидация формы (form.is_valid())
  • получение готового словаря с данными формы (form.cleaned_data)


3. Standalone models стайл

Во втором примере мы явно описывали, какие поля должна иметь форма. Давайте вместо этого перейдём на новый уровень абстракции — опишем нашего клиента через standalone-модель:
class RuspostClient(models.Model):
    name = models.CharField('имя', max_length=64)
    address = models.CharField('адрес', max_length=255)
    index = models.CharField('индекс', max_length=6)

Мы задали понятие «Клиент» (RuspostClient) через класс так, как мы его понимаем — имя, адрес, индекс, тип полей и длина. Нас не сильно-то волнует, какие типы полей должны быть в форме, потому что формы для клиента мы теперь автоматически создаём одной строчкой:
from django.forms.models import modelform_factory
form_client = modelform_factory(RuspostClient)(request.POST or None)

Но мы также можем и более точно описать, что мы хотим от формы:
from django.forms.models import modelform_factory
form_client = modelform_factory(
    RuspostClient,
    widgets={
        'name': TextInput(attrs={'style': 'width: 300px;'}),  # Для этих полей задать ширину 300 px
        'address': Textarea(attrs={'style': 'width: 300px;'}),
    },
    exclude=('index', )
)(
    request.POST or {  # Заполни поля формы либо данными из POST (если они есть)...
        'name': 'Аноним',   # ... либо значениями по умолчанию, которые здесь указаны
        'address': 'Ул. Пушкина, дом Калатушкина',
    }
)

Полученную форму мы можем проверить и сохранить полученную модель:
if form_client.is_valid():
    client = form_client.save(commit=False)  # Без commit=False будет ошибка, т.к. в БД мы сохранить не можем

Фактически, мы получили ту же самую форму, что и в пункте 2. Так ради чего была вообще введена модель?

  • На выходе мы получили не словарь, а инстанс модели django. Отличия инстанса класса от словаря очевидны, но, как минимум, инстанс может иметь методы. Мы переходим от функционального программирования к ООП — согласитесь, что куда приятней (и правильней) вызывать client.send_email_notification(), чем send_email_notification(client).
  • Мы генерируем формы «на лету». Если в одном случае мне нужно создать форму с одними полями (fieldset1), а в другом случае — с другими (fieldset2), то я просто напишу:
    form_client1 = modelform_factory(
        RuspostClient,
        fields=fieldset1,
    )
    form_client2 = modelform_factory(
        RuspostClient,
        fields=fieldset2,
    )
    


Кстати, полученный инстанс мы таки можем преобразовать в словарь и вернуться к своему корыту из пункта 2: ruspost_client_dict = model_to_dict(ruspost_client)

Выводы


Мы с вами рассмотрели модели в Django, которые не хранятся в базе данных. Они:

  1. классы
  2. почти не отличаются от обычных моделей django
  3. удобны для описания временных объектов (хранить их не получится)
  4. дают все преимущества объектно-ориентированного программирования
  5. удобны для автоматического создания форм на лету для временных объектов (не нужно создавать несколько классов в forms.py)
  6. позволяют абстрагироваться от ручного описания полей формы, т.е. задают инвариант (а вот уже генерирование формы позволяет выбрать варианты)

комментарии (17)

+4
+5 –1
Bteam ,  

Ох что? Ну нужны Вам классы и ОПП, так возьмите форму из пункта 2 и переопределите ей метод clean, и пусть она возвращает уже заполненный класс RusPost, зачем сюда приплетать модели? Мягко говоря это бред.

0
+1 –1
alexander007 ,  

Согласен. Концепция модели django извращается при таком подходе.

0
+1 –1
kesn ,  

Не совсем понял, не могли бы пояснить, какая именно концепция извращается?

+1
alexander007 ,   * (был изменён)

Такой юскейс придется документировать. Т.е. любому другому человеку будет трудно понять зачем тут модель. В доках django подобное использование моделей не описано.

+2
kesn ,  

Спасибо за критику :)
Можно и так. Но
1) Переопределять у формы clean() и возвращать не стандартный список form.cleaned_data, а какой-то объект — как по мне, так это немного отходит от документации django. Кроме того, представьте, что я хочу этими данными заполнять не только класс RusPost, но и класс ClientEmailSurvey — теперь мне из clean() возвращать (RusPost, ClientEmailSurvey)?
2) При вашем подходе ООП как раз-таки теряется — модель RusPostClient не создаётся.

Мне нравилось во всём этом что? Что я мыслю объектами. Есть объект «клиент». Создаём для него форму, её заполняют, на выходе получаем заполненный объект «клиент». Что я с ним потом дальше сделаю — знает только моя левая пятка.

В данном случае такой подход позволяет разделить собственно библиотеку и форму, то есть:

Форма --> Библиотека
заменяется на
Форма --> Клиент --> Библиотека

Когда это нужно, а когда нет — отдельный вопрос.

0
+1 –1
andy128k ,  

1) необязательно переопределять


def give_me_my_model(self):
    return self.cleaned_data

2) роль модели — персистентность, здесь она не востребована а потому модель избыточна.

class MyModel(object): pass

class MyForm(Form):
    ...
    def give_me_my_model(self):
        m = MyModel()
        m.__dict__.update(self.cleaned_data)
        return m
+1
kesn ,  

Мы просто подходим к проблеме с разных сторон.

Вы создаёте основу — форму, и фактически из неё создаёте модель.
Плюсы:
— это не извращение
— это достаточно просто
Минусы:
— если нужно несколько разных форм для клиента — нужен абстрактный класс с give_me_my_model, остальные формы от него наследуются (всё прописываем в forms.py)
class MyModel(object): pass — по этому коду (не заглядывая в код формы) скажите, что содержит в себе класс MyModel.

Я создаю модель и из неё генерирую форму.
Плюсы:
— чётко описано, что такое «клиент»
— генерируется любое подмножество форм для клиента
Минусы:
— это извращение в терминах моделей django

0
andy128k ,  

>… нужен абстрактный класс…

Нет, не нужен. Это Python, язык с динамической типизацией.

> что содержит в себе класс MyModel

То же, что и форма. Я-то обошёлся бы и словарём. Он ничем не хуже объекта. Класс я привёл на случай если вам захочется добавить ему методов. Если хотите каких-то гарантий существования полей, то вполне можно написать и так


class MyModel(object):
    name = address = index = None


> чётко описано, что такое «клиент»

Форма для этого ничуть не хуже. Такой же класс.

> генерируется любое подмножество форм для клиента

В простых случаях, да. Но что если валидаторы на одно и то же поле в разных формах разное? Формы наследуются ничуть не хуже, даже лучше.
0
kesn ,  

> Нет, не нужен. Это Python, язык с динамической типизацией.
Прошу прощения, имелся в виду, конечно же, базовый класс

> В простых случаях, да. Но что если валидаторы на одно и то же поле в разных формах разное? Формы наследуются ничуть не хуже, даже лучше.
Тут согласен — валидаторы так лихо не задать.

По сути, мы с вами предлагаем 2 варианта:

class Client(Model):
    name = CharField(...)
    def method(self):
        ...
form = modelform_factory(Client)(request.POST)
client = form.save(commit=False)


# (возможны вариации)
class Client():
    def method(self):
        ...
class Form(Form):
    name = CharField(...)
form = Form(request.POST)
client = Client()
client.__dict__.update(form.cleaned_data)


Первый мне кажется более логичным. Всё-таки мне кажется, что всё, относящееся к клиенту, должно описываться «в нём», а не в его форме. Возможно, это пережитки C++.
0
andy128k ,  

> базовый класс
> пережитки C++

Да :)

> относящееся к клиенту, должно описываться «в нём»

Не возражаю. Но вот поля, валидаторы и виджеты, это всё-таки не «клиент», а способ его ввода/вывода и не относится к нему никак.

0
+1 –1
monIToringe ,  

Если уж так хочется использовать модели без таблицы в БД, лучше вместо хака с местоположением models.py, использовать Meta-атрибут модели managed.
Названия методов camelCase-ом? Серьезно?

0
kesn ,  

managed… Ох, ё!.. Не знаю, как я проплыл мимо этой части документации. Спасибо, это точно лучше грязных хаков.

Мне почему-то казалось, что стиль написания названий методов не регламентирован. Но camel case я подцепил в документации reportlab:

# String drawing methods
canvas.drawString(x, y, text):
canvas.drawRightString(x, y, text)
canvas.drawCentredString(x, y, text)
+4
monIToringe ,  

Конечно регламентирован: http://www.python.org/dev/peps/pep-0008/.
Сamel case встречается только в старых либах или в тех, где это сделано для совместимости с другими языками (unittest, например).

–1
nvbn ,  

Блин, даже на знаю какую ссылку на документацию или исходники джанги вам кинуть.
Но смотрите в сторону определения аппов, нахождения моделей, форм и просто что такое метаклассы в python.
А то у вас чудо-ад =)

+4
kivsiak ,  

Откройте для себя WTForms и SQLAlchemy. Гораздо приятнее в использовании.

0
istinspring ,   * (был изменён)

хотел написать как раз, а уже без меня справились. плюсую.

0
kesn ,  

Благодарю, про WTForms слышу впервые