https://habr.com/ru/post/481042/
Прошло уже несколько недель, как официально вышла 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. Некоторые коды написаны в старой нотации, надеюсь на понимание, не всегда находится время или сотрудники на рефакторинг.