Упрощая жизнь c Django

    За время использования Django я накопил множество небольших инструментов: декораторов, шорткатов, кастомных полей и просто утилит, которые кочевали со мной из проекта в проект в виде сборного пакета handy. В конце концов, я решил поделится своим опытом, потому как такой код — это и есть материализованный опыт (даже лучше — код можно исполнить), и открыть наиболее полезные куски handy для всех желающих.

    Пакет направлен на уменьшение необходимого boilerplate при использовании фреймворка джанго. На то, чтобы избавить от необходимости писать одно и то же раз за разом, сделать код короче и выразительней.

    Итак, установим пакет:

    pip install handy
    

    и начнём. Что же мы получаем используя handy?

    1. Избегаем возни с HttpResponse и render_to_response()
    с помощью декоратора @render_to():

    # Эта вьюха выдаст страницу сгенерированную по шаблону ‘app_name/foo.html’,
    # где app_name - имя текущего приложения
    @render_to()
    def foo(request):
       return {
           'bar': Bar.objects.all()
           # Можно легко переопределить шаблон, HTTP код, тип контента и добавить куки
           'STATUS': 410,
           'CONTENT_TYPE': 'text/plain'
       }
    

    2. JSON-вьюхи в пару строк:

    @render_to_json()
    def posts_by_tag(request, tag=None):
       posts = Post.object.values().filter(tag=tag)
       return list(posts)
    

    3. И ещё более высокоуровневая обёртка для обработки более сложных асинхронных запросов:

    @ajax
    @ajax.login_required
    @ajax.catch(Post.DoesNotExist)
    def enable_post(request):
       post = Post.objects.get(pk=request.GET['id'])
    
       if post.author != request.user:
           # отправляет {"success": false, "error": "permission_denied"}
           raise ajax.error('permission_denied')
    
       post.enabled = True
       post.save()
       # ответ {"success": true, "data": null} при успешном выходе
    

    4. Отправляем письмо сгенерированные по шаблону одной строчкой:

    render_to_email(article.author.email, 'approved.html', {'article': article})
    

    Шаблон письма может содержать тему и любые другие заголовки.

    5. Коллекцию полей модели и соответствующих полей форм и виджетов:

    DAYS = zip(range(7), 'Sun Mon Tue Wed Thu Fri Sat'.split())
    
    class Company(models.Model):
       phones = StringArrayField('Phone numbers', blank=True, default='{}')
       workdays = IntegerArrayField('Work days', choices=DAYS)
    
    company = Company(phones=['234-5016', '516-2314'], workdays=[1,2,3,4])
    company.save()
    

    В форме phones будут выведены в текстовом поле через запятую, а workdays в виде нескольких чекбоксов.

    Также есть JSONField для хранения произвольных данных и другие поля.

    И ещё:
    — middleware, которое срезает ненужные пробелы из html-ответов,
    — общий master-slave роутер,
    — небольшая обёртка для упрощения логирования,
    — и пара текстовых и отладочных утилит.

    Делитесь опытом


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

    Ссылки: Github, PyPi, Django Packages и документация на английском.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 78

      +4
      большинство таких задач решается через CBV.

      обычно создаю базовый класс от TemplateView с методами:

      * reverse
      * redirect
      * json_response
      * get_object_or_404
      * оберктой над django.contrib.messages вида self.messages.success('Hello, man!')
      * get_model
      и др.

      для login_required так же делается класс-потомок от предыдущего с переопределенным методом dispatch.

      все это избавляет от кучи импортов в каждом views.py и добавляет удобства в работе.
        0
        Кажется, Вы перемудрили :-)

        get_object_or_404 — это как get_object() в DetailView. Что такое get_model не очень из контекста ясно, но наверняка в List/Detail не пригодится. А вместо json_response, я считаю, лучше пользовать микшин.
          0
          from django.db.models import get_model
          
          entries = get_model('auth', 'User').objects.get(...)
          
            0
            а теперь сделайте DetailView с login_required :)

            можно через urls.py, но это зачастую размазывает логику по urls.py и views.py
              0
              Ох, сколько раз мне это приходилось делать…

                0
                Ох уж эти новомодные горячие клавиши.

                class LoginRequiredMixin(object):
                    @ method_decorator(login_required) # пробел после собачки надо убрать.
                    def dispatch(self, request, *args, **kwargs):
                        return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
                
                class PostDetailView(LoginRequiredMixin, DetailView):
                    model = Post
                
                  0
                  все круто и юзабельно, пока не нужно добавить свою логику в login_required. например, логгирование попыток входа.

                  а за DetailView — отдельное спасибо, не внимательно читал доки, не знал за него.
                    +1
                    развидьте предыдущий комментарий на счет попыток входа, фигню написал, туплю в конце недели.
                      +1
                      Я показал лишь пример LoginRequiredMixin. Разумеется, можно сделать более общий микшин UserPassesTestMixin и организовать его по образу и подобию аналогичного декоратора.

                      class LoginRequiredMixin(object):
                          @ method_decorator(login_required) # пробел после собачки надо убрать.
                          def dispatch(self, request, *args, **kwargs):
                               if self.test():
                                  return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
                               else:
                                   return self.test_failed()
                      
                          def test_failed(self):
                              "Response в случае ошибки"
                      
                          def test(self):
                              return True
                      


                      И уже потом от него наследовать LoginRequired ;)

                      class LoginRequiredMixin(UserPassesTestMixin):
                          def test(self):
                              return self.request.user.is_authenticated()
                      


                      Главное — вовремя остановиться =)
              +4
              Классы сложнее функций, а композиция проще, чем наследование. Я выбираю более простой способ писать вьюхи. Я допускаю, что возможна ситуация когда код на CBV будет проще, но сам с таким не сталкивался.

              В любом случае, считать функциональные вьюхи устаревшими попросту неправильно.
                –2
                С другой стороны необходимость кучи импортов раздражает. Я даже подумывал сделать спец. пакет, сложить туда всё дерьмо и делать:
                from handy.shits import *
                
                • UFO just landed and posted this here
                    –2
                    ...
                    Although practicality beats purity.
                    ...
                    
                    • UFO just landed and posted this here
                        –5
                        Два экрана импортов — это одновременно непрактично и безобразно, а вот import * как раз стандартная практика в джанго.
                  +2
                  Вот что думает один из core developerов Django по поводу CBV: lukeplant.me.uk/blog/posts/djangos-cbvs-were-a-mistake/.
                  И это не только его мнение, классы сложны в поддержке и понимании. А сила Pythonа в том, что код легко понимается с одного взгляда.
                  –4
                  post = Post.objects.get(pk=request.GET['id'])
                  


                  вы действительно используете такую конструкцию в реальных проектах? без валидации?
                    0
                    Думаю, там всё ок:

                    ajax.catch(Post.DoesNotExist)

                    Однако глаз коробит, согласен.
                      –3
                      ?id=ololotrololo';delete from auth_users;
                      


                      во так например
                        +2
                        Джанго экранирует подставляемые значения.
                          0
                          В данном случае нет: джанга попробует привести значение pk к целому (через int</code). У неё это, разумеется, не получится.
                            0
                            ok. но KeyError в определенных случаях здесь обеспечен
                            0
                            Не сообразил =)

                            Впрочем, удаления тут не произойдёт. Будет некрасивая «пятисотка», но да, это плохо. Согласен.
                              0
                              А разве django не использует preparedStements для запросов?
                                +1
                                Нет, не использует, он использует клиенты для бд, которые следуют Python Database API, в котором экранирование параметров осуществляется на клиенте средствами этих библиотек.

                                В результате SQL-инъекции при запросе через ORM не будет.
                                  0
                                  Ясно, получается, что в случае MySQL и MySQLdb мы не сможем использовать server-side prepared statements
                                    0
                                    Ну через ORM нет, а так там есть расширения, только зачем?
                                      0
                                      Запросы будут бегать быстрее, ведь не надо будет каждый раз строить план выполнения запроса
                                        0
                                        Зато планы будут неоптимальны, потому как будут строиться без знания конкретных значений.
                                          0
                                          Откуда такая информация? Как по вашему происходит выполнения запроса в СУБД?
                                            0
                                            Примерно так — пришедший запрос парсится в какое-то внутреннее представление, потом для него готовится план выполнения. При составлении плана могут использоваться значения в условиях, например, если какое-то условие выберет небольшое количество рядов (по прогнозам), то будет использоваться соответствующий индекс, если много, то просто последовательное сканирование.
                                              0
                                              Я неверно выразился — под планом выполнения имел ввиду разобраное SQL выражения. Это еще не план. Но уже и не исходный SQL. Т.е. в случае server-side prepared statement второй раз не придется заниматься разбором sql-а, так как внутреннее представление уже есть
                                              0
                                              Всё это относится к PostgreSQL:
                                              wiki.postgresql.org/wiki/FAQ#Why_is_my_query_much_slower_when_run_as_a_prepared_query.3F
                                              В MySQL возможно планирование попроще и выиграть на подготовленных запросах действительно можно.
                                                0
                                                Хм, очень странно такое поведение Postgres-а. Ересь какая-то. Просто очень плотно работал с oracle, для которого неиспользование PreparedStatement-ов приводит к очень грустным результатам. Может просто не надо делать оптимизацию на уровне компилирования запроса, а уж если очень хочется — строить общий план, а потом корректировать на основании полученных данных. По крайней мере мы избегаем компиляции запроса, что для сложных запросов даст припрост.
                                                  0
                                                  Ну в 9.1, вроде, добавили перестройку плана при получении реальных параметров, но я не вижу где тут можно много времени выиграть — парсинг запроса не такая уж сложная вещь.
                                                    0
                                                    Ну у вас получается разобраный запрос во внутреннем представлении и план на основе синтаксиса запроса. Уже будет выигрыш. А если запрос большой? Интерпретация его каждый раз тоже небесплатна
                                                      0
                                                      Ну да, но это не так уж и медленно, а вот если план неэффективен, то можно замедлиться сразу в тысячи раз.

                                                      Да и большинство юзкейсов покрывают INSERT… VALUES… и UPDATE… VALUES…
                                                        0
                                                        Я так понял план неправильно считает постгре, и то как Вы написали-они поправили это дело. Mysql строит не весь план в случае prepared statements. Так что то, что preparedstatements медленные в постгре можно отнести к недальновидности разработчиков постгряшного оптимизатора.

                                                        Что касается юзкейсов — Вы почему то опустили селекты, которые в данном случае намного интереснее. Если у Вас селект в полстраницы — его разбирать уже не так быстро.
                                0
                                500-ку можно сделать красивой. Выдавать 500 в н. ештатной ситуации вполне адекватно. Более того, если возникнет KeyError, то это будет означать, что проблема не здесь, а где-то ещё, и замалчивать её неправильно. DoesNotExists наоборот надо ловить, потому как оно отражает штатную ситуацию — устаревшую ссылку
                                  0
                                  500-ка это внутренняя ошибка сервера. Ключевое слово — внутренняя. То есть если БД упала, например, или место на диске закончилось. Как-то нехорошо, когда внутренняя ошибка провоцируется внешним воздействием.
                                    0
                                    Вопрос восприятия, у нас концепция такова, что 500я должна выдаваться если произошло что-то неожиданное, что не было предусмотрено. И одновременно уходит письмо разработчикам. Такой подход позволяет быстро находить и чинить многие баги.
                                      0
                                      В этом случае любой кшольник-кулхацкер, запустивший сканер поиска ошибок, просто засыпет спамом :-) А если отчёты будут отправляться синхронно, ещё и отказ в обслуживании вызовет
                                        +1
                                        Когда запустит тогда и будем смотреть, и адекватно отвечать. Решать проблему до её возникновения в корне неверно. А если просто ловить все такие ошибки, то это может привести к замалчиванию неправильного поведения, битых ссылок и т.п.

                                        Опыт показывает, что при замалчивании ошибок искать такие проколы по жалобам пользователей довольно сложно и небыстро.
                                          0
                                          Решать проблему заранее, может, и не следует. А вот от канонических дыр лучше бы закрыться. Кстати, забыл спросить, а почему в том примере айдишник берётся из request.POST?
                                          +2
                                          Бывало такое, что просыпаешься, а на почте 1500 писем с одной и той же ошибкой. Ну и что? Ошибка быстро исправлена, вреда эти 1500 писем не принесли (Gmail и миллион бы пережил).

                                          А если замалчивать ошибку, то они и дальше бы оставались на сайте. В конце концов что важней — работающий продукт или чистота инбокса?
                                            –1
                                            Работающий продукт важнее. Поэтому и надо ошибок не допускать. Понятно, что от них никто не застрахован, но самые основные лучше зарубать на корню.
                                0
                                Уверен, что у большинства есть своя bulletproof-библиотека с полезными функциями. Но досадно, что народ с завидным упорством плодит всё новые и новые пакеты: django-annoying, django-extensions, handy. А ведь функциональность зачастую перекрывается. Тот же render_to или json_response не реализовывал только ленивый.

                                И вместе с тем, очень многие из этих библиотек словно из прошлого века. В смысле, заточены под старые версии Джанги.

                                За упрощение жизни: многие в повседневной жизни используют CBV. Для них катастрофически не хватает микшинов с функциональностью, аналогичной декораторам. Или абстрактных моделей, в которых уже добавлены какие-то полезные колонки, которые приходится каждый раз добавлять руками (типа date_created).
                                  0
                                  > Для них катастрофически не хватает микшинов с функциональностью, аналогичной декораторам.
                                  Эти иногда пользую github.com/brack3t/django-braces
                                    0
                                    Спасибо. Но даже если и использовать, всё равно как-то грустно, что придётся подключать django-braces для микшинов, django-annoying для get_object_or_None и django-extensions для чего-нибудь ещё.

                                    Вот бы один метапакет, в котором это всё в одном месте было… Не совсем джанго-вэй, конечно, но его можно разбить на аппы и подключать только нужное:

                                    INSTALLED_APPS = (
                                        'metapacket.views',
                                        'metapacket.shortcuts',
                                        'metapacket.decorators',
                                    )
                                    


                                    Вроде и красиво, и зависимостей не плодит.
                                  0
                                  Некрасиво, что декоратором render_to нельзя воспользоваться, не поставив пустые скобки. Посмотрите на login_required, например.
                                    0
                                    Декоратором render_to вообще пользоваться моветон, на мой взгляд. Ладно бы жили в каменные времена версии 1.2, когда это было вынужденной мерой: тогда шорткаты были длиннее собственно функций, на которые ссылались.

                                    Начиная с 1.3 есть django.shortcuts.render. А в 1.2, кстати, можно было импортировать direct_to_template as render. Получалось вполне себе универсально.
                                      0
                                      Декоратор render_to принимает аргументы, поэтому нужны скобки.
                                        +1
                                        login_required тоже. И хотя «Explicit is better than implicit», есть ещё и «Beautiful is better than ugly» (The Zen of Python). Кроме того мы находимся в контексте джанги, поэтому стоит соблюдать единообразие. И третий аргумент — такое поведение реализуется довольно просто: django/contrib/auth/decorators.py. Поэтому мне кажется, что такой подход будет удачнее.
                                          0
                                          Я думал об этом, но в конце концов не стал реализовывать, это усложнит реализацию и сделает поведение не таким очевидным, стоит ли оно того, чтобы не писать пары скобок?
                                      +6
                                      Декораторы render_to и json_response есть в django-annoying. Полезная библиотека. Но насчет render_to — это уже действительно устарело. Сейчас можно написать просто:
                                      return render(request, 'template/template.html', {'a':2,' b':3})
                                      

                                      По мне это как то более… не знаю как сказать. Нагляднее что ли.
                                        0
                                        И если у тебя несколько выходов из вьюхи, то придётся имя шаблона повторять, либо вынести в переменную. Так что всё спорно.

                                        Это просто другой способ смотреть на вещи — отдельно логика, отдельно рендеринг. Кроме того, @render_to() предлагает более высокий уровень факторизации, например похожие страницы для зарегистрированных и для анонимов могут использовать общую логику:

                                        def _edit(request):
                                            # логика редактирования какой-нибудь штуки
                                            # будет использоваться повторно
                                        
                                        # создаём вьюхи из логики и других аспектов
                                        my_edit = login_required(render_to('my/edit.html')(_edit))
                                        edit = render_to()(_edit)
                                        
                                          0
                                          Если у вьюхи несколько выходов, то почти наверняка лучше использовать CBV: отлаживать и писать тесты намного проще.
                                            +3
                                            Если несколько выходов на один и тот же шаблон, значит у вас формируется несколько различных вариантов параметров контекста. Логично значит внутри условий добавлять в контекст параметры, а потом в конце функции отрендерить данный контекст при помощи шаблона.

                                            Если же у вас посередине функции в рендомных местах стоят return-ы, то эта функция по определению нуждается в рефакторинге.
                                              0
                                              Ну структурном программировании можно долго спорить, я не считаю его такой уж безусловно правильной идеей. По мне, код должен отражать, то как удобно думать о решении, и если удобней думать «если так, то сразу выкидываем, если сяк, то выводим сообщение, а иначе — обычная обработка», то и код должен выглядеть именно так.

                                              Каждый раз когда мы не выходим из функции сразу, мы вынуждены вводить новый элемент состояния (переменную/флажок), который занимает внимание, читающего такой код, программиста, а это весьма ограниченный ресурс.
                                            0
                                            Хотя шорткат render(), конечно, уменьшает надобность в @render_to().
                                            +1
                                            … и ни одного теста?)
                                              +4
                                              Буду занудствовать.

                                              1. Вот такое:
                                              <hh user=render_to>()
                                              def foo(request):
                                                 return {
                                                 }
                                              


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

                                              2. last_modified — а если в вьюха уже установила Last-Modified?

                                              3. class StripWhitespace(object):
                                              компиляцию регеспов вынести в модуль.

                                              4. render_to_email; if settings.DEBUG:
                                              Зачем? есть же console.EmailBackend

                                              В общем, еще пилить и пилить :)

                                              Ну а в целом — кнопка watch уже нажата, удачного развития!
                                                –2
                                                1. Один из вариантов декораторов — это композиция, и естественно они будут менять семантику декорируемой функции. foo здесь уже не вьюха, а только её часть и если думать о ней так, то вполне логично, что и выглядит она по другому.

                                                2. Такой проблемы не возникало, а следовательно и решать её преждевременно.

                                                4. console.EmailBackend выводит на консоль уже кодированное письмо, т.е. русский текст не прочитаешь. Решение конечно ad-hoc, правильнее было бы свой EmailBackend написать.
                                                  0
                                                  В целом спасибо за конструктив )
                                                  +1
                                                  Честно говоря, я против того, чтобы декоратор менял тип и логику выходных данных функций. Декоратор — как приправа, должен делать функцию «вкуснее», однако в его отсутствии блюдо все равно должно быть съедомным.

                                                  Например,
                                                  ajax
                                                  def enable_post(request):
                                                  ...
                                                  raise ajax.error('permission_denied')


                                                  легко может быть заменено на:
                                                  def enable_post(request):
                                                  ...
                                                  return ErrorAjaxResponse('permission_denied')


                                                  При этом сохранив оригинальный дух и стиль кодирования Django (View возвращает объект Response) и снизив порог вхождения для новичков.
                                                    0
                                                    Никто не запрещает, но мне удобно думать, что у меня отдельно есть обработчик бизнес логики, а отдельно другие аспекты — контроль доступа, обработка ошибок и представление. То, что я могу их отделить — однозначно хорошо, как это делать в виде утилит и шорткатов, которые вызываются по мере надобности, в виде композиции функций с использованием синтаксиса декораторов или с помощью наследования классов дело вкуса.

                                                    P.S. Можно использовать <source lang="python">...</source> для подстветки кода и &#x40; для создания собак.
                                                      +1
                                                      Разделение логики и представления это хорошо. Тут все верно. Но причем тут декораторы?

                                                      Декор — (от лат. decorare — украшать) — дополнительные элементы в живописных композиционных сюжетах.


                                                      Т.е. когда у Вас есть здание и вы его декорируете — это значит что вы делаете красивым готовое решение. Вы же не поручаете декоративным гипсовым колоннам поддерживать потолок. Без декора здание будет некрасивым — однако оно будет, и оно будет работать.

                                                      А для разделения логики и представления есть модули, инкапсуляция, наследование, функциональные паттерны и другие вещи. Да и return SomeCostomResponse() даже короче в написании чем raise SomeCustomError в функции с дополнительной оберткой.
                                                        0
                                                        Кстати для шаблонов есть вполне красивое решение от самой джанги:
                                                        class MyCustomView(TemplateView):
                                                        
                                                            template_name = 'my_custom_template.html'
                                                            def get_context_data(self, **kwargs):
                                                                return {
                                                                    'bar': Bar.objects.all()
                                                                }
                                                        


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

                                                          Simple is better than complex.
                                                          Flat is better than nested.

                                                          Другими словами если функция делает дело, то создавать класс, который| содержит несколько методов, которые сделают дело просто излишне.
                                                          0
                                                          Ну может декораторы и не совсем правильное название, но они делают дело, да и не я их называл. Так что аргумент не принимается.

                                                          Что же качается return и raise, то где вы увидели SomeCustomError()? А использование исключений позволяет выносить отдельные вещи в подфункции, декораторы и возвращать {«success»: true} просто по успешному завершению функции. В остальном же дело вкуса и спорить об этом глупо.
                                                        +1
                                                        Зачем все это в одном пакете? Почему не в нескольких? Вообще же никакой связности.
                                                          0
                                                          Связность есть — мелкие нужные штуки, слабая, конечно, но поддерживать кучу пакетов, а соответственно привносить кучу сущностей — тоже сомнительное решение.
                                                            0
                                                            Хм, а в чем именно сомнительность? Всегда был уверен, что куча маленьких узкоспециализированных аппов лучше одного монструозного.

                                                            Еще, по поводу StripWhitespace middleware. Чем {% spaceless %} не угодил?
                                                              0
                                                              Он решает несколько другую задачу, да и засорять свои шаблоны не хочется. Использование middleware чище.
                                                                0
                                                                Этот тег отлично наследуется, так что достаточно одного раза в базовом шаблоне. Но даже если вы уверены, что middleware чище, почему бы не использовать django.utils.html.strip_spaces_between_tags, зачем писать эту логику самостоятельно?
                                                        0
                                                        > DAYS = zip(range(7), 'Sun Mon Tue Wed Thu Fri Sat'.split())

                                                        можно заменить на

                                                        DAYS = tuple(enumerate('Sun Mon Tue Wed Thu Fri Sat'.split()))

                                                        Only users with full accounts can post comments. Log in, please.