Чуть-чуть «извращений» над моделями django

в 2:17, , рубрики: decorators, django, models, python, метки: , , ,

Лень двигатель прогресса

Иногда, создавая модели в django, я себя начинаю чувствовать мартышкой. Постоянно создаю атрибут enable, который принимает по умолчанию то значение True, то False. Меняю менеджер objects на свой простой EnableManager. А хочется иметь механизм, который эти монотонные операции делал за меня. Раз хочется, то можно сделать.

Представляем, что мы хотим получить

Первая мысль, пришедшая мне в голову, была: написать несколько абстрактных классов(если писать 'mixin', то django-модель не добавляет поля, заданные нами), которые потом я буду подключать, когда мне это необходимо. Это достаточно дубовый метод, который плодит огромное количество повторяющегося кода, чего мне не хотелось.

Следом я подумал про функцию, которая будет создавать мне нужные классы. Это избавит от огромного количества повторяющегося кода, и в целом выглядит не плохо. «А что если, где-то не стоит использовать, скажем, твой менеджер, и это единственный случай?» — крутилось у меня в голове. Ради эксклюзивного случая создавать класс не очень хочется. Если использовать декоратор, то и этого можно избежать.

Итого: нам нужно создать функцию, которая при определенных параметрах:

  • Возвращает абстрактный класс
  • Декорирует класс, добавляя в него нужное поле и нужный менеджер

Везде есть подводные камни

В данном случае, это было декорирования класса. Дела в том, что в декораторе, используя '@foo' мы можем передать только, над которым будет совершенно действие. Но как сделать так, что бы мы могли передать параметры декоратору?
Дело в том, что синтаксическая конструкция '@' используется как композиция двух функций(g•f(x)). В python'e и функция и класс является обьектом, и мы можем вернуть функцию синтаксической конструкции '@', т.е. примерно так (l(y))•(f(x)), где l(y) — возвращает функцию. Код декоратора, будет примерно таким:

def foo(cls=None, param="DefaultValue"):
    def decorator(cls):
        # do something with class
        return cls
    if cls is None:
        return decorator
    else:
        return decorator(cls)

Что то похожее, только с функциями, используется в django для декоратора login_required.

Теперь можно и покодить

Вроде все подводные камни разобраны, теперь можно приступить к написанию кода:

def Enable(cls=None, status=True, set_manager=True, mixin=False):
    '''
    Adds enable field into cls or return mixin
    :param cls: class that would updates
    :param status: default value for enable field
    :type status: bool
    :param set_manager: sets is EnableManager is required
    :type set_manager: bool
    :param mixin: sets should be returns model mixin
    :type mixin: bool
    '''
    def decorator(cls):
        '''
        Adds field and manager if manager is required
        '''
        cls.add_to_class('enable',
                         models.BooleanField(_('Enable'), default=status))
        if set_manager:
            cls.add_to_class('objects', EnableManager())
        return cls

    class Class(models.Model):
        '''
        Enable AbstractModels
        '''
        enbale = models.BooleanField(_('Enable'), default=status)

        class Meta:
            abstract = True

    if cls is None and mixin:
        if set_manager:
            Class.add_to_class('objects', EnableManager())
        return Class
    elif cls is None and not mixin:
        return decorator
    elif cls and not mixin:
        return decorator(cls)
    elif cls and mixin:
        raise DecoratorMixinException

DecoratorMixinException — это исключение, которое говорит о том что функция не может быть вызвана как декоратор с параметром mixin = True.

Для добавления менеджеров используется метод add_to_class(), особенность django-моделей, если писать в классе objects = YourManager() или cls.objects = YourManager(), то работать не будет. Через этот же метод мы и добавляем нашей модели необходимое поле.

Примеры использования

EnableFalseMixin = Enable(status=false, mixin=True)

class SimpleModel(EnableFalseMixin,  models.Model):
    '''
    Simple model
    '''
    # some field here

@Enable
class TestFalseEnable(models.Model):
    '''
    Test enable
    '''
    # some fields here

@Enable(status=false)
class TestFalseEnable(models.Model):
    '''
    Test enable
    '''
    # some fields here

Данная статья задумывалась, как пример создания удобного механизма, которым можно пользоваться повседневно.
Надеюсь кому-нибудь она будет полезна. Приятной вам разработки на фреймворке django.

P.S.
Кажется, некоторые внимательные люди будут ругаться, что имя функции «Enable» написано с большой буквы, что не соответствует PEP8. Я это сделал, потому что данная функция порождает новый класс. Пожалуйста, мастера python'а и django, скажите на сколько правильно это сделано? Встречал такое в некоторых проектах, но никогда не задумывался о том на сколько это правильно.

Автор: Zapix

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js