Как стать автором
Обновить

Нас Django вкус волнует и манит

Время на прочтение 6 мин
Количество просмотров 8.8K
Кушаем кактус

Прошло уже несколько недель, как официально вышла 3 версия Django. Я работал с этой версией ещё до публикации официального релиза и, к сожалению, заметил, что развитие Django сильно замедлилось. Версия 1.3 от 1.7 отличается в разы, а вот 3 версия содержит косметические изменения ветки 2 и не более.

Мой проект winePad стартовал с версии Django 1.3, и к текущему моменту в нем переопределено около 12% внутреннего кода Django.

Видя код новой версии, я понимаю, что правки, которые я или мои коллеги сделали при работе с предыдущими версиями поедут и дальше. А глядя на roadmap и вялотекущие изменения официального репозитория ждать, что ошибки будут скорректированы в будущих версиях — не приходится.

Вот о работе над ошибками я и хочу рассказать:

Метод get


Мало кто догадывается о том, что в стандартном методе get django с самого начала была ошибка. Метод get должен вернуть вам либо один объект либо предупредить о том что найдено несколько объектов или же сообщить, что объектов нет.

1  def get(self, *args, **kwargs):
2         clone = self._chain() if self.query.combinator else self.filter(*args, **kwargs)
3         if self.query.can_filter() and not self.query.distinct_fields:
4             clone = clone.order_by()
5         limit = None
6         if not clone.query.select_for_update or connections[clone.db].features.supports_select_for_update_with_limit:
7             limit = MAX_GET_RESULTS
8             clone.query.set_limits(high=limit)
9         num = len(clone)
10        if num == 1:
11            return clone._result_cache[0]
12        if not num:
13            raise self.model.DoesNotExist()
14        raise self.model.MultipleObjectsReturned()

Строка 9 получает данные по ВСЕМ записям которые указаны в queryset и переводит их в набор объектов. Об этом есть предупреждение в документации.

До 3 версии ограничения на количество запрашиваемых объектов не было. Это означает, что get получал абсолютно все данные и превращал их в объекты, прежде, чем дать предупреждение что объектов много.

В итоге вы могли получить несколько миллионов объектов в памяти, только для того, чтобы узнать, что найден более, чем 1 объект. Сейчас появились строки 5,7,8. И теперь вы гордо получите только MAX_GET_RESULTS=21 объект, прежде чем узнать, что объектов больше чем 1.
При тяжёлом "__init__" задержка будет значительной. Как лечить:
Переопределить MAX_GET_RESULTS в django.db.models.query.py
переопределить GET или перед вызовом GET использовать:

vars(queryset.query).update({'high_mark':2, 'low_mark':0})
или
queryset.query.set_limits(0,2)

Метод __init__ моделей Django


Не совсем понятно объявление в коде встроенного метода __init__

_setattr=setattr

вероятно, это для псевдоубыстрения кода переносом ссылки на функцию в локальный словарь, но речь не об этом. Проблем несколько:

1. Если передать дополнительные пары аттрибут=значение в __init__ модели вы получите «got an unexpected keyword argument».

В таком случае я предлагаю не утяжелять метод __init__ а делать добавление атрибутов после инициализации:

obj = MyClass()
vars(obj).update({'attr':val, 'attr2':val2 ...})

2. В новой Django добавили возможность переопределения дескрипторов на любое поле модели (Field.descriptor_class). Но ни один дескриптор не знает, инициализирован объект, или ещё нет. Это надо, например, если дескриптор будет использовать данные из prefetch_related объектов, которые появятся только после инициализации главного объекта.

Использовать сигнал окончания инициализации мне не нравится, поскольку подписчиков может быть очень много.

В таком случае я не придумал ничего умнее, чем переопределить __init__ и добавить аттрибут окончания инициализации.

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._end_init = True

Querysets


Жестко прописанные EmptyQuerySet/DateQuerySet убраны, уже хорошо. Однако ситуация с queryset в роли менеджера мне идеологически не нравится.

Если я хочу переопределить класс QuerySet создаваемых менеджерами, я добавляю атрибут _queryset_class

class MyManager(models.Manager):
    _queryset_class = MyQuerySet

Внимание, для старых версий это не работает, можно сделать, например, так:

class MyManager(models.Manager):
    def get_query_set(self):
        response = super(MyManager, self).get_query_set()
        response.__class__ = MyQuerySet
        return response

inlineFormset панели администратора


Проблем несколько:

1. Нельзя отобразить стандартным inlineFormset записи не имеющие напрямую связь с главным объектом формы. Например: форма правки цен товара. В середине лежит справочная статистическая Tabularinline форма закупочных оптовых цен на «подобный» товар.

Решается переопределением метода get_formset inline модели и созданием собственного MyFormSet унаследованного от BaseInlineFormSet

def get_formset(self, request, obj=None, **kwargs):
        kwargs['formset'] = MyFormSet
        super().get_formset(request, obj, **kwargs)

class MyFormSet(BaseInlineFormSet):
    pass

2. Если вы правите обьект с inlineformset в админ панели, а в это время кто-то удалит одну из записей обьектов внутри inlineformset через другой механизм, то вы получите ошибку и сохранить обьект не удастся. Только через kopy paste в новом окне браузера.

Я нашел пока только одно Решение — не использовать inlineformset.

Панель администратора


«Киллер-фича» Django, является самым большим кактусом проекта:

1. Действие «Удаление объектов» в администраторах моделей видны по умолчанию, не важно, есть ли права у пользователя на удаление, или нет.

Решается отключением этого действия по умолчанию:

admin.site.disable_action('delete_selected')

2. Создание дополнительных прав пользователя из админ панели будет невозможно пока вы не включите администратор модели Permissions:

from django.contrib.auth.models import Permission
class PermissionsAdmin(admin.ModelAdmin):
	search_fields = ('name', 'codename','content_type__app_label', 'content_type__model')
	list_display = ('name', 'codename',)
	actions = None
admin.site.register(Permission, PermissionsAdmin)  

3. Увы, прав на доступ только к определенным объектам в Django не существует.

Это возможно решить через прописывание записи в djangoAdminLog со специальным флагом.
А после проверять наличие флага:

user.logentry_set.filter(action_flag=ENABLED, .....).exists()

4. Если вы создаете действия администратора, так, как стоит в документации, то помните, что они не протоколируются в djangoAdminLog автоматически.

5. Еще недостаток этой части документации — все примеры только на функциях. А как же GCBV? В моих проектах все действия администраторов моделей переведены на GCBV. Репозиторий.

Подключение действия стандартно:

class MyAdmin(admin.ModelAdmin):
    actions = (MyActionBasedOnActionView.as_view(),)

ContentType — реестр моделей django


50% гениальность / 50% тупость.
Ни у одной модели нет доступа к реестру моделей по умолчанию.
У нас в проектах решается добавлением миксина во все классы:

from django.contrib.contenttypes.models import ContentType
class ExportMixin(object):
    @classmethod
    def ct(cls):
        if not hasattr(cls, '_ct'):
            cls._ct, create = ContentType.objects.get_or_create(**cls.get_app_model_dict())
            if create:
                cls._ct.name = cls._ct.model._meta.verbose_name
                cls._ct.save()
        return cls._ct

    @classmethod
    def get_model_name(cls):
        if not hasattr(cls, '_model_name'):
            cls._model_name = cls.__name__.lower()
        return cls._model_name

    @classmethod
    def get_app_name(cls):
        if not hasattr(cls, '_app_name'):
            cls._app_name = cls._meta.app_label.lower()
        return cls._app_name

    @classmethod
    def get_app_model_dict(cls):
        if not hasattr(cls, '_format_kwargs'):
            cls._format_kwargs = {'app_label': cls.get_app_name(), 'model': cls.get_model_name()}
        return cls._format_kwargs

теперь мы можем вызывать obj.ct() при необходимости.

UserModel


Возможность переопределения модели пользователя появилась в версии 1.5.

Но к 3 версии так и не исправили model=User в стандартных UserCreationForm/UserChangeForm.
Решается:

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
class MyUserCreationForm(UserCreationForm):
    class Meta:
        model= get_user_model()

Система переводов


Разметка текстов, видимых пользователю, выполняется тегами

{% trans %}

или в коде через

gettext_lazy

Однако, управление этими ресурсами в админпанели отсутствует. Вообще.

Есть внешние решения, все они работают кое-как.

Например, Rosetta систематически теряет тексты, и глючно работает интерфейс. Нигде нет проверки прав доступа к переводам. Для работы необходимы систематические makemessages / compilemessages…

В winePad теги trans, blocktrans и gettext_lazy переопределены и мы стали получать тексты из кеша. Если нет кеша, то кешированный запрос из базы get_or_create избавил нас и от makemessages.

Тема мультиязычности — вообще сложная. Встроенное в Django решение работает только для статических текстов. А ведь есть еще необходимость перевода данных моделей. Я попробовал по-своему решить вопрос перевода динамических текстов в проекте django-TOF, где я соединил возможности model-translation и Parler/Hvad. Вероятно, кому-то будет интересно заглянуть.

Пока я остановлю повествование, поскольку статья по исправлению недостатков Django легко может превратится в longread.

Прошу вас рассказать, как вы улучшаете свою Django. Если будет продолжение, я систематизирую появившиеся идеи.

p.s. Некоторые коды написаны в старой нотации, надеюсь на понимание, не всегда находится время или сотрудники на рефакторинг.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+15
Комментарии 31
Комментарии Комментарии 31

Публикации

Истории

Работа

Python разработчик
142 вакансии
Data Scientist
75 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн