Рецепты от ПанГурмана

    Недавно запустили сервис по бронированию ресторанов ПанГурман. Внутри это более-менее типичный django-сайт. Попробую рассказать, как там что устроено (с картинками). В статье не будет ничего супер-хитрого, но, надеюсь, кому-нибудь пара трюков или идей покажутся полезными и как-то упростят жизнь.

    Набор инструментов стандартный — PostgreSQL, Django 1.4 (+GeoDjango), Sentry, Celery (+redis), Fabric.

    Стартовая страница


    image

    На стартовой странице ничего необычного: форма для регистрации. Как уже писал, формы-вьюхи самопальные, django-registration выпилил с негодованием после того, как понял, что не использую оттуда ни одной вьюхи или формы, пытаюсь заманкипатчить модель, а баг-трекер у проекта отключен :)

    При регистрации отправляется письмо с приветствием, оно сверстано как html. Для отправки html-писем используем templated-emails: в templates есть папка emails, в которой хранятся все письма. Каждое письмо — в своей папке, содержащей 3 файла: email.html (html-версия письма), email.txt (текстовая версия письма) и short.txt (тема).

    image

    Отправляются письма вот так:
    send_templated_email([user], 'emails/activation', context=context)
    

    в список адресатов можно передать User-ов или просто емейл-адреса. Ерунда, но просто и удобно, на разных проектах используем уже довольно долго этот подход.

    С html-письмами еще одна проблема есть — не очень понятно, как их при разработке смотреть без постоянной отправки себе. Чтоб локально проверять, используем django-eml-email-backend (тупейшее приложение с email backend'ом, сохраняющим письма в eml-файлы вместо отправки). Eml-файлы большинство почтовых клиентов открывают без проблем (на маке даже по пробелу предпросмотр есть).

    Ок, зарегались


    После регистрации и настройки аккаунта человек попадает на главную страницу.

    image

    На ней можно обратить внимание на форму поиска.

    image

    Элементы изначально были сверстаны примерно так:

        <div class="b-filter_selector time">
            <select name="persons" data-behavior="Chosen">
                <option value="1">1 персону</option>
                <option value="2" selected="selected>2 персоны</option>
                <option value="3">3 персоны</option>
            </select>
        </div>
    


    Ребята стараются придерживаться БЭМ, инпуты нередко заворачиваются в div и им часто нужно приписывать какие-то css-классы, data-атрибуты и тд.

    Зашивать html в питоньем коде — дурной тон, html должен быть в шаблонах. Нормального встроенного средства влиять на рендеринг элементов форм в шаблоне в джанге нет (а нам тут нужно добавить data-behavior к полю). Из-за таких штук очень активно используем django-widget-tweaks (bitbucket, github) при прикручивании верстки, это приложение позволяет процесс прикручивания упростить и обескостылить.

    <div class="b-filter_selector time">
        {% render_field search_form.persons data-behavior='Chosen' %}
    </div>
    


    Если нужна какая-то стандартная обвязка для поля, делаем html-сниппет, который можно потом подключать через {% include %} (примерно так):

        {% comment %}
        Пример:
    
            {% include "inc/input.html" with field=form.query div_classes="search" no_label=0 %}
    
        {% endcomment %}
        {% load widget_tweaks %}
        {% if no_label != 1 %}{{ field.label_tag }}{% endif %}
        <div class="b-input {{ div_classes }} {% if field.errors %}error{% endif %}">
            {{ field|add_class:'b-input_input' }}
        </div>
    
    

    То же самое для вывода ошибок.

    {% render_field %} из django-widget-tweaks умеет также дописывать атрибуты (css-классы, например) к уже имеющимся:

        {% render_field form.emails rows="2" class+="b-input_textarea gray" autofocus="" %}
        {% include "inc/errors.html" with errors=form.emails.errors %}
    


    Блок «Доступны сегодня»


    image

    В этом блоке у нас 5 ресторанов, которые доступны для бронирования сегодня. Получаются они вот так:

    restaurants = Restaurant.objects.enabled().free_today_in_city(city).prefetch_related('scenes')[:5]
    


    Как видите, тут строится цепочка своих методов (.enabled().free_today_in_city(city)). Встроенный джанговский подход — вынесение таких штук в менеджер — этого делать не позволяет, т.к. методы менеджера возвращают QuerySet, у которого кастомных методов уже нет — для фильтрации, получается, можно только 1 свой метод использовать.

    Чтоб обойти это ограничение, используем PassThroughManager из django-model-utils и пишем QuerySet вместо менеджера:

        class RestaurantQueryset(QuerySet):
            def enabled(self):
                return self.filter(enabled=True)
    
            def free_today_in_city(self, city):
                today = city.now().date()
                return self.filter(city=city).free(today)
    
            def free(self, date):
                # ...
    
        class Restaurant(models.Model):
            # ...
            objects = PassThroughManager.for_queryset_class(RestaurantQueryset)()
    
    


    Эдакие «менеджеры на стероидах» получаются (картинку решил не подбирать :)). Их по всему проекту вместо стандартных менеджеров используем.

    Кликаем на «Посмотреть все»


    … и попадаем на страницу со списком ресторанов. На этой странице большая форма поиска, результаты обновляются аяксом.

    image

    У меня есть небольшой личный «пунктик» по поводу js: сайтом должно быть можно пользоваться без js. Это дает при разработке некоторые преимущества. Помимо очевидных (ошибка в js не делает сайт неработоспособным) есть и менее очевидные: например, такой сайт проще тестировать (можно больше всего проверить без помощи selenium) + возможно, проще писать)

    На данный момент используем следующий подход к аяксу: аяксовые и обычные запросы обрабатывает одна и та же вьюха, разница только в шаблоне. Т.к. аяксовый шаблон — это часть обычного шаблона, то и подключаем его в обычном шаблоне. Обычный шаблон:

        {% extends 'base.html' %}
        <!-- обвязка: шапка, форма поиска и тд -->
        ...
        <div class="b-search-results_result">
            {% include 'restaurants/ajax/list.html' %}
        </div>
        ...
        <!-- обвязка: форма поиска, подвал и тд -->
    


    Аяксовый шаблон:

        <ul class="b-search-results_result_list">
        {% for restaurant in restaurants %}
            <li>...</li>
        {% endfor %}
        </ul>
    


    Шаблон выбирается на основе request.is_ajax(); на клиенте есть декларативный js, который умеет любую обычную html-форму превращать в аяксовую:

        def restaurant_list(request, city_slug):
            # ...
            response = TemplateResponse(request, 'restaurants/list.html', {...})
            if request.is_ajax():
                response.template_name = 'restaurants/ajax/list.html'
            return response
    


    В ПанГурмане решил немного с этим поэкспериментировать. Сначала это был декоратор @ajax_template, который добавлял вьюхе аяксовость:

        @ajax_template('restaurants/ajax/list.html')
        def restaurant_list(request, city_slug):
            # ...
            return TemplateResponse(request, 'restaurants/list.html', {...})
    


    Т.е. заворачиваешь обычную вьюху в декоратор, прописываешь data-атрибут форме и все: форма становится аяксовой.

    Но в этом варианте повторяется 'restaurants/(ajax/)?list.html' (что надоедает и является источником ошибок), + у вьюхи может быть несколько точек возврата и не все нужно делать аяксовыми (а значит вариант хуже, чем простая проверка без декоратора).

    Поэтому от @ajax_template отказались и пока остановились на таком варианте:

        def restaurant_list(request, city_slug):
            # ...
            return show(request, 'restaurants/list.html', {...})
    


    show — это наследник от TemplateResponse, который для аяксовых запросов пробует сначала шаблон в подпапке ajax. См. https://gist.github.com/2416695

    show имеет еще пару методов, которых мне всегда не хватало — но, возможно, зря не хватало, т.к. с ним есть пара тонкостей и не все замечательно (например, .with_context упадет, если предыдущая вьюха вернула редирект), но использовать вполне можно (да и к аяксу проблемы имеют мало отношения).

    Короче говоря, делаем сайт, как будто ajax-а не существует, а добавляем его потом, где нужно, вынесением нужной части шаблона в подпапку.

    Что-то нашли, тыкаем в ресторан


    Попадаем на страницу ресторана.

    image

    На ней тоже есть аякс (в форме заказа через ajax-запросы проверяется доступное время), но реализован он еще другим способом :)

    Календарик строится на клиенте (без js будут обычные поля для ввода) и поэтому проще с сервера получать json, а не html. С помощью django-tastypie по-быстрому соорудил API, и через него json нужный отдается. Это не сильно дольше, чем написать вьюху, отдающую json, руками, но API все равно пригодится + разные плюшки вроде троттлинга и форматов выдачи нахаляву получаем.

    В другом проекте использовали для API django-piston; субъективно — django-tastypie приятнее.

    Очень примитивная (и не вполне полная) справка к API генерируется автоматически такой вот вьюхой:

        def _api_resources(api):
            resources = {}
            api_name = api.api_name
    
            for name in sorted(api._registry.keys()):
                resource = api._registry[name]
                resources[name] = {
                    'list_endpoint': api._build_reverse_url("api_dispatch_list", kwargs={
                        'api_name': api_name,
                        'resource_name': name,
                    }),
                    'schema': api._build_reverse_url("api_get_schema", kwargs={
                        'api_name': api_name,
                        'resource_name': name,
                    }),
                    'doc': resource.__doc__,
                    'resource': resource
                }
            return resources
    
    
        def browse(request, api):
            resources = _api_resources(api)
            return TemplateResponse(request, 'tasty_browser/index.html', {
                'api': api,
                'resources': resources
            })
    
    


    Шаблон:

        <h1>API {{ api.api_name }}</h1>
        <table class='tastypie-api'>
            <tr><th>Resource</th><th>url</th><th>Structure</th><th>Description</th></tr>
            {% for name, resource in resources.items %}
                <tr>
                    <td>{{ name }}</td>
                    <td>
                        <a href='{{ resource.list_endpoint }}?format=json'>
                        {{ resource.list_endpoint }}</a>
                    </td>
                    <td>
                        <a href='{{ resource.schema }}?format=json'>
                        {{ resource.schema }}</a>
                    </td>
                    <td>
                        {{ resource.doc }}
                    </td>
                </tr>
            {% endfor %}
        </table>
    
    


    Можно посмотреть тут: http://pangurman.ru/api-docs/

    Оплата прикручена через django-robokassa (потом еще варианты добавятся), смс-уведомления о брони — через imobis + celery.

    Финансы


    image

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

    Начитавшись Фаулера, уже несколько лет использую следующий подход к реализации «личного счета»: сумма на счете нигде не хранится (впрочем, может кешироваться иногда), в базе храним операции на личном счете. Это позволяет естественным образом получить историю операций + уберегает от ошибок.

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

    Подобная модель кочует из проекта в проект с небольшими изменениями:

        class MoneyTransfer(models.Model):
    
            PURCHASE = 'purchase'
            ROBOKASSA = 'robokassa'
            INVITE_BONUS = 'invite-bonus'
            REFUND = 'refund'
            PROMOCODE = 'promocode'
    
            TRANSFER_TYPE_CHOICES = (
                (u'Снятие со счета', (
                    (PURCHASE, u'Оплата бронирования'),
                )),
                (u'Пополнение счета', (
                    (ROBOKASSA, u'Платеж через Робокассу'),
                    (INVITE_BONUS, u'Бонус за приглашение друга'),
                    (PROMOCODE, u'Активация промокода'),
                    (REFUND, u'Возврат'),
                )),
            )
    
            user = models.ForeignKey(User, verbose_name=u'Пользователь')
            amount = models.DecimalField(u'Сумма', max_digits=10, decimal_places=2)
            created_at = models.DateTimeField(u'Дата/время операции', auto_now_add=True)
            comment = models.CharField(u'Описание', max_length=255, blank=True, null=True)
    
            transfer_type = models.CharField(u'Тип', max_length=20,
                                             null=True, blank=True,
                                             choices=TRANSFER_TYPE_CHOICES)
    
            content_type = models.ForeignKey(ContentType, null=True, blank=True)
            object_id = models.PositiveIntegerField(null=True, blank=True)
            reason = GenericForeignKey('content_type', 'object_id')
    


    Тут можно обратить внимание на способ задания choices. Писать больше, зато нет магических констант и по ошибке не запишешь в базу 'invite_bonus' вместо 'invite-bonus' — MoneyTransfer.INVITE_BONUS всегда один и сразу упадет с AttributeError, если его написать неправильно (да и автокомплит появляется).

    Столик-то забронировали, но стоп, а как это работает?


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

    image

    В django для этого есть flatpages, но мы обычно используем другой подход: все страницы делаем обычными вьюхами с нормальными шаблонами, храним все в VCS, а куски текста, которые нужно редактировать, помечаем блоками из django-flatblocks:

        <div class="b-text">
            {% flatblock "how-it-works" 600 %}
        </div>
    


    Для админов при наведении мышки на текст показывается ссылка на редактирование
    (это сделано путем задания своего шаблона flatblocks/flatblock.html):

        {% load url from future %}
        {% load markup %}
    
        {% if user.is_staff %}
            <div class="b-flatblock-edit">
                <a class="flatblock-edit-link markitup-flatblocks-edit-icon"
                   title = "Редактировать блок"
                   href='{% url "edit_flatblock" flatblock.pk %}?next={{request.path}}'>
                </a>
                {{ flatblock.content|markdown:"video" }}
            </div>
        {% else %}
            {{ flatblock.content|markdown:"video" }}
        {% endif %}
    


    В блоках хратится разметка в markdown, на странице редактирования показываем виджет из django-markitup (с предпросмотром):

    image

    Все тексты на сайте, в которых допустима какая-то разметка, редактируются с помощью django-markitup (вдохновенные описания ресторанов с фотографиями, например). Фильтр markdown настроен таким образом, что не трогает html-теги, так что при необходимости в текст можно добавить любую разметку.
    А, еще расширение прикручено ( python-markdown-video ), которое ссылки на youtube или vimeo в ролики превращает — по-хорошему это через oembed делать, наверное нужно.

    Панель для ресторанов


    Для ресторанов есть специальная панель, через которую они сейчас могут управлять временем работы (когда столики доступны, когда нет). Панель реализована через еще один отдельный экземпляр AdminSite. Туда можнут попасть пользователи, которые назначены «менеджерами ресторана». Тонкость в том, что у этих пользователей не должно быть флажка is_staff, а то они смогут заходить в обычную админку, чего бы не хотелось. А так — обычная кастомизированная админка.

    [rant on]
    Часто пишут, что, мол, джанговская админка негибкая и тд. — так и не понял, что люди конкретно имеют в виду :) Админка предоставляет CRUD, верстку и дизайн «из коробки», и если знать, что можно настроить, а что сложно, то проблем не возникает. В качестве православных альтернатив обычно предлагают какие-то фреймворки для написания дэшбордов — так это штука перпендикулярная CRUD, и для джанговской админки такие фреймворки есть (django-admin-tools, nexus).

    Если какое-то взаимодействие не укладывается в CRUD — никаких проблем, ничто не мешает написать свою вьюху и поставить на нее ссылку откуда-то из админки (readonly_fields и list_display тут часто удобны; можно и шаблон переопределить). Выкинуть или переписать админку желания за несколько лет ни разу не возникало, используем ее во всех проектах, экономит кучу времени. Возможно, специфика проектов такая, или я действительно что-то очевидное упускаю, не знаю.
    [rant off]

    В админке вместо raw_id_fields активно используем приложение со страшным названием django-salmonella, которое «заражает» виджеты для выбора связанных записей полезным поведением (для FK и M2M:

    image

    image

    Кроме id объекта становится видно его название (обновляется аяксом); по клику на название можно сразу попасть на страничку изменения объекта, что тоже удобно.

    Тесты


    Для тестов стали использовать factory-boy (форк), про который уже было на хабре. Со времени той записи в factory-boy появилась одна супер-полезная штука: RelatedFactory. Это как SubFactory, только наоборот (например, можно сделать фабрику для пользователя, которая будет сразу создавать ему профиль). Документацию можно почитать тут. Пример:

    class ProfileF(factory.DjangoModelFactory):
        FACTORY_FOR = Profile
        city = factory.SubFactory(CityF)
        balance = 0
    
        @classmethod
        def _prepare(cls, create, **kwargs):
            # поддержка 'balance=555'
            balance = kwargs.pop('balance')
            profile = super(ProfileF, cls)._prepare(create, **kwargs)
            if balance > 0:
                MoneyTransferF(user=profile.user, amount=balance)
            return profile
    
    class UserF(factory.DjangoModelFactory):
        FACTORY_FOR = User
    
        username = factory.Sequence(lambda n: "username%s" % n)
        first_name = factory.Sequence(lambda n: u"Имя %s" % n)
        last_name = factory.Sequence(lambda n: u"Фамилия %s" % n)
        email = factory.Sequence(lambda n: "email%s@example.com" % n)
        is_staff = False
        is_active = False
        is_superuser = False
        password = '123'
        profile = factory.RelatedFactory(ProfileF, 'user')
    
        @classmethod
        def _prepare(cls, create, **kwargs):
            kwargs['password'] = make_password(kwargs['password'])
            return super(UserF, cls)._prepare(create, **kwargs)
    
    


    Теперь в тестах можно так писать:

    user = UserF(profile__balance=100)
    


    В качестве разминки сделал в factory-boy pull request с добавлением поддержки 3го питона, но 3й питон — это тема для отдельного рассказа)

    В Django 1.4 с тестами есть еще одна хитрость. По умолчанию для хеширования паролей стал использоваться алгоритм PBKDF2. У PBKDF2 одной из характеристик, обеспечивающих защищенность, является низкая скорость работы. Но кроме защищенности низкая скорость работы обеспечивает еще низкую скорость работы :) На основном сайте это совсем не проблема (пользователи не так часто регистрируются-логинятся, да и задержка невелика), а вот в тестах оказалось, что это штука критичная, особенно если использовать factory-boy и создавать много пользователей, назначая им пароль через хэш.

    В случае ПанГурмана установка вот эта строчка в test_settings.py ускорила тесты в 2 раза:

        PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
    
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 38
      +23
      Образец правильной рекламы на техническом ресурсе.
        +1
        Отличная статья, как всегда на уровне. Спасибо!

        Ну и пару вопросов, как водится.

        По поводу аякса: кажется, можно было использовать django-pjax от Jacob Kaplan-Moss. Оно вроде бы делает ровно то же самое, но всё-таки велосипедом меньше :-)

        По поводу виджетов и форм: мне кажется, что нет ничего страшного в том, чтобы задавать data-параметр в питоне: это всё-таки информация, а не её представление. Твик виджетов несколько усложнил для восприятия шаблоны. Или, как вариант, есть хорошее приложение для улучшения форм — django-floppyforms. Оно как раз реализует виджеты именно на шаблонах. Ну и, конечно, из коробки идёт несколько полезных HTML5-виджетов.
          0
          Да, django-pjax — примерно то же самое. Там клиент на jQuery, а у нас mootools+behavior используется. Серверная часть там тривиальная в общем-то.

          Насчет усложнения шаблонов, верстка:

          <input type='text' name='email' id='id_email' class='b-input' placeholder='Введите email'>
          

          превращается в

          {% render_field form.email class='b-input' placeholder='Введите email' %}
          

          вроде проще некуда, кастомные атрибут скопипастить можно)

          В django-floppyforms хорошо то, что вся верстка в шаблонах, а не в питоне. Но с django-floppyforms все равно для изменения стандартного поведения рендеринга (== прикручивания верстки) нужно питоний код трогать и еще один шаблон определять, в отличие от django-widget-tweaks.

            0
            в django-pjax, кстати, подход, аналогичный декоратору ajax_request — мне кажется, наследник TemplateResponse все-таки решает задачу лучше (меньше кода + более гибко).
              0
              Раньше пользовался django-pjax а потом перелез на django-tastypie + backbone-tastypie
                +1
                Сравнивать pjax и backbone слегка некорректно. Первый — обёртка над браузерным History API. Второй — полноценный MVC-фреймворк. Да, в нём есть роутер, но использовать всю эту машинерию только ради аяксовых страничек… неразумно.

                С другой стороны, если Вам нужен полноценный REST API, то и аяксовая навигация как таковая не нужна =)
                  0
                  Разумеется я не стал бы прикручивать столько ради аякс навигации — django-tastypie уже был в проекте изначально, а потом решили добавить backbone ну и только после этого отказались от pjax в пользу уже имеющегося функционала backbone. Оставалось только подружить tastypie с backbone и это было backbone-tastypie.
                  Кому интересно, тут есть пример проекта django-tastypie + backbone-tastypie. А еще вот один из хороших примеров django сайта где используется tastypie + backbone
                    0
                    «Атомной артилерией» все равно является dojox.data.JsonRestStore + dojango, в сочетании с Django-Piston, хотя, в свете последнего Class-based generic views, Django-Piston уже не особо нужен, ибо REST-API можно организовать и нативными средствами Django.

                    Хотя да, backbone подкупает своей компромисностью функционала и простоты. Наверное стоит упомянуть тогда и про github.com/af/djangbone
              +2
              Для того, кто питонирует с джанго подобные чтения висьма полезны. Полезно и интересно посмотреть, как другие решают распространенны проблемы, связанные с особенностями этого питоновского фреймворка.
                0
                Отлично написано, вот так и надо писать о разработке!
                Жду когда, кто-нибудь о RoR проекте так же напишет.
                  0
                  У меня текст поплыл. Изображение.
                    +4
                    Спасибо! Завел в треке баг №103 «у чувака с хабра текст поплыл», думаю, скоро починим)
                      0
                      Мы попробовали исправить, если не сложно, можете посмотреть, починилось ли? И заодно, какая у вас ось/браузер?

                      Пробовал написать в личку, но хабр ведет себя потрясно — is.gd/gBPlA1

                      Спасибо!
                        0
                        Баг действительно исправлен, спасибо. У меня Ubuntu 12.04 c увеличенными шрифтами.
                      0
                      Спасибо за крутую статью. Приятно и легко читалось. Хороший пример, как правильно нужно «готовить» Django. Многие говорят, что для больших проект джанги уже не хватает. Например у нас на проекте от джанги осталась только система пакетов, шаблонизатор, middlewares и наверное все. Остальное все самописное. Эволюционно как-то все менялось. Но многие большие проекты хорошо ложатся под Django. Например та же Instagram.
                        0
                        Спасибо!

                        Из «больших» на джанге можно еще, кстати, pinterest отметить — в январе 2012 у него больше трафика было, чем у LinkedIn, YouTube или Google+ — вот уж действительно большой сайт на джанге :) Пишут, что используют «patched django», а что именно patched — не отвечают :)

                        У нас как-то так получается, что в основном не заменяем части джанги полностью, а надстраиваем. В шаблонах/формах — widget-tweaks, в моделях — model-utils, в тестах — factory-boy и django-webtest, обработка ошибок — sentry как логгер и т.д. Некоторые части не используем в принципе (contrib.comments, свои виджеты для форм, form preview, form wizard, flatpages, humanize, serialization, sites), но в остальном обычная джанга — так код понятнее и проще поддерживать (т.к. опыт с джангой есть у многих).
                          0
                          Да, class-based-views тоже не используем. Используем вместо них TemplateResponse (кстати, в других фреймворках аналогов TemplateResponse не видел).
                        0
                        Еще есть пару вопросов:
                        1) А на чем крутиться сайт?
                        2) Как деплоите проект?
                        3) Какую стратегию ведения веток используете в репозитории? И используете ли вообще?
                        4) Производительность: какое посещение сайта сейчас, какие нагрузки на сервер получаются и какая тачка у вас на продакшне?

                        Спасибо :)
                          0
                          Про деплоинг kmike уже писал.
                            0
                            Как раз об этом в следующей статье написать хотел, пол-стать уже готово) Про django-fab-deploy уже писал, но там поменялось многое, и несколько тонкостей есть. Сайт — на дебиане + апаче + mod_wsgi; деплоим fab push:migrate,pip_update из консоли; ветки используем, но меня не совсем устраивает, как (ветки production и testing для продакшн- и тестового сервера + ветки под фичи без спец. значений); сейчас нагрузка не очень большая, запустились недавно и рекламы никакой не было пока; запас прочности есть; сервер — hetzner 4S за 60$ в месяц (с линода перелезли) — дофига ресурсов за такие деньги.
                              0
                              А почему перелезли с linode? Используем его, вроде проблем нет.
                                0
                                Мне линод тоже нравится, и проблем с ним не было. Но у hetzner ресурсов сильно больше за те же $. В линоде за 60$ VPS с 1.5 RAM и 60Gb диска, в хетцнере за 60$ выделенный сервер с 32GB RAM и 2x3TB raid — искушение велико.
                                  0
                                  Действительно впечатляет, как бы это только не сказалось на качестве — «бесплатный» сыр… :)
                            0
                            Увидев кнопку входа через facebook, подумал что странно что нет вконтакте, твиттеров итд… какие причины были выбрать только facebook?
                              +1
                              Они будут слегка попозже. Будет даже мейлру.
                                0
                                Ага, все делалось довольно быстро, «на скорость», методом вырезания и откладывания фич, и там много чего еще доделать можно.
                                  0
                                  А для социального входа используете что-нибудь типа django-social-auth, django-allauth? или с нуля под каждый тип?
                              0
                              Михаил, Вы пишите классные модули, двигайте правильные темы (особенно понравились django-widget-tweaks), но я ещё с pymorphy заметил, что Вы совсем не следите за пробелами внутри аргументов методов/функций. Как вот тут, например, у choices
                                      transfer_type = models.CharField(u'Тип', max_length=20,
                                                                       null=True, blank=True,
                                                                       choices = TRANSFER_TYPE_CHOICES)
                              


                              Не по pep8 же. Да и просто не красиво выглядит… Заведите у себя pep8/pyflakes ради интереса хотя бы)

                              Извиняюсь, что не совсем по теме, просто давно уже хотел сказать Вам, да повода не было.
                                +1
                                Баг-репорт и/или pull request про это в любой из проектов с радостью приму! Мне кстати комрады, с кем работаю (@obiwanus, например), тоже часто пишут, что вот именно с пробелами вокруг аргументов не всегда последователен :) pep8 уважаю, невнимательность стараюсь исправлять.
                                  0
                                  Для pymorphy как-нибудь оформлю pull request — очень уж он мне нравится! :)
                                  Кстати, у Вас github или bitbucket основной? Или оба равносильны?
                                    0
                                    Пулл реквест — без разницы, где вам удобнее, там. Багтрекер основной — на битбакете.
                                  +1
                                  Кстати, если параметры функции идут в несколько строк, то лучше выглядит, если вокруг равно есть пробелы.

                                  Например,

                                  names = dict(
                                      vasia="petya",
                                      vova="serezha",
                                      mashenka="sveta",
                                  )
                                  


                                  имхо, выглядит хуже, чем
                                  names = dict(
                                      vasia = "petya",
                                      vova = "serezha",
                                      mashenka = "sveta",
                                  )
                                  


                                  [ворчун mode]

                                  > Михаил, Вы пишите классные модули, двигайте правильные темы
                                  У вас русский язык тоже иногда не по pep8 :)

                                  [/ворчун mode]
                                    0
                                    А Вы заведите где-нибудь рядом с девелоперским сервером Jenkins, не исключая такие ошибки в настройках — быстро отпадёт желание писать пробелы у аргументов :) Увидите там Violations (pylint +600) и перехочется так писать)))
                                    У меня, по крайней мере, так и было. Причём самый веский аргумент так не делать — чтобы не было возможности выпускать в продакшен проекты с ошибками.
                                      0
                                      У нас «code review» ручной (просто коммиты друг друга читаем регулярно), пишут все неплохо, не вижу смысла в каких-то дополнительных механических методах, они разработку в небольшой команде не ускорят imho.

                                      Насчет ошибок — тесты пишем, не то чтобы фанатики, но покрытие процентов 80 есть, тесты прогоняются автоматом при каждой выкладке на сервер (да и локально гоняем их), вот это помогает.
                                  +1
                                  А если не секрет, сколько по времени длилась разработка сервиса такого уровня? И сколько девелоперов принимало участие?

                                  Спасибо…
                                    0
                                    Делать начали в марте; первое время делали вдвоем с верстальщицей, потом еще человек подключился. К апрелю, в принципе, пользоваться сайтом с точки зрения пользователя было можно (24 марта — первая бронь), но многих вещей не хватало (вроде смс, входа через фейсбук и панели для ресторанов); мы публично тогда не запускались, но более-менее рабочий сайт упростил и ускорил заключение соглашений с ресторанами (а смысл в сервисе бронирования без них).

                                    Ну и не стоит говорить о разработке в прошедшем времени, все самое интересное только начинается, останавливаться на достигнутом мы не намерены :)
                                    0
                                    Спасибо за отличную статью

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                    Самое читаемое