Показаны сообщения с ярлыком django. Показать все сообщения
Показаны сообщения с ярлыком django. Показать все сообщения

8 ноября 2012 г.

Валидация Django моделей, пять советов

Не думаю, что для кого-то открою Америку, сказав что в Django есть валидация не только для форм, а и для моделей :) Однако судя по моей практике это чуть спорное, но весьма полезное решение не спешат повсеместно использовать и городят свои велосипеды для проверки значений при создании/обновлении моделей. Поэтому хочу поделиться несколькими простыми советами по этой теме.

Во-первых, храните валидаторы отдельно от моделей. Хранение всех валидаторов в models.py неимоверно раздувает и без того не маленький модуль моделей (а иногда это и пакет), хранение же валидаторов в validators.py решает эту проблему и логично выносит все операции по проверке значений/уникальности модели в подходящее для этого место. Ну и сюда же, не пишите сложные проверки в Model.clean, Model.validate_unique методах, просто вызывайте необходимые функции валидации из validators.py, например:

models.py

from django.db import models

from .validators import validate_complex_case, validate_unique


class Card(models.Model):
    ...
    def clean(self):
        validate_complex_case(instance)

    def validate_unique(self, exclude=None):
        validate_unique(self)
        return super(Card, self).validate_unique(exclude)

validators.py

from django.core.exceptions import ValidationError


def validate_complex_case(instance):
    if instance.name == 'XXX' and instance.path != '/path/to/XXX':
        raise ValidationError('Please provide proper path for the card.')


def validate_unique(instance):
    manager = instance._default_manager
    other = manager.filter(name=instance.name)

    if other.count():
        message = 'Card with this Name already exists.'
        raise ValidationError({'__all__': [message]})

Во-вторых, непонятно зачем, но ошибки в проверке на уникальность модели нужно обворачивать в message_dict, использование простого сообщения приводит к AttributeError. Хотя с другой стороны это повышает возможности по написанию ошибок в разных полях моделей. Можно, например, подсветить какие именно поля не уникальны и уже сохранены в БД.

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

from django.core.validators import MaxLengthValidator, MinLengthValidator
from django.db import models

from .validators import validate_markup


class Card(models.Model):
    ...
    comment = models.TextField(validators=[
        MinLengthValidator(10), MaxLengthValidator(1000), validate_markup
    ])

В-четвертых, валидация модели автоматически не вызывается, когда вы хотите сохранить модель не из админ-панели или ModelForm'ы. Т.е., просто добавление валидаторов в поля модели и задание методов clean или validate_unique не гарантирует, что неправильные данные не сохранятся в базе данных после Model.objects.create или instance.save(). Для того, чтобы обезопасить себя и включить валидацию и для моделей, используйте сигналы:

from django.db import models
from django.db.models import signals
from django.dispatch import receiver


class Card(models.Model)
    ...


@receiver(signals.pre_save, sender=Card)
def validate_card(instance):
    instance.full_clean()

Ну и в-пятых,

  • Не забывайте про стандартные валидаторы Django
  • Импортируйте исключение ошибки валидации как from django.core.exceptions import ValidationError
  • Для моделей нельзя писать методы clean_FIELD как для форм
  • Валидация модели при вызове instance.full_clean() идет в следующей последовательности: валидация полей (instance.clean_fields()), вызов instance.clean(), проверка на уникальность (instance.validate_unique())

зы. Код и принципы данного материала актуальны для Django 1.3 ветки (да я так и не пересел на 1.4 или 1.5 потому что не вижу в этом особого смысла), может в новых версиях что-то поменялось, уточняйте в документации :)

21 мая 2012 г.

Наконец-то, django-discover-runner от Янниса Лейдла

Эта была долгая история полная костылей, но кажется лед тронулся и совсем скоро и Django'вский дефолтный тест раннер будет иметь поддержку автоматического поиска тестов в проекте и мы наконец забудем про from test_* import * как про страшный сон.

Само решение и соответствующий тикет появились довольно давно, а вчера Яннис Лейдл (jezdez, один из Django core-team) оформил решение как отдельный пакет, назвав его django-discover-runner. Шаг в верную сторону, но для меня до сих пор не понятно, почему это очевидное изменение не было сделано сразу после включения unittest2 в проект.

5 октября 2011 г.

django.dispatch.receiver - FTW!

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

from django.contrib.auth.models import User
from django.db.models import signals

...

signals.post_save.connect(auto_create_user_profile, sender=User)

Однако с выходом Django 1.3 ситуация поменялась. Сейчас достаточно задекорировать функцию сигнала, в нашем случае auto_create_user_profile, с помощью @receiver декоратора:

from django.contrib.auth.models import User
from django.dispatch import receiver

...

@receiver(signals.post_save, sender=User)
def auto_create_user_profile(instnance, **kwargs):
    ...

И все, готово! Согласитесь, удобней и легче чем раньше.

зы. Удачного рефакторинга! ;)

8 сентября 2011 г.

Запускаем тесты для проекта, который использует Django 1.3 и django-celery

В оффициальной документации django-celery есть раздел о том, как правильно настроить Celery для тестирования с Django проектом, также там упоминается и о возможности использовании специального тест раннера из пакета djcelery.

Так вот, если вы используете Django 1.3 - забудьте про эту возможность до лучших времен. Ее использование черевато DeprecationWarning'ами и не совсем понятной работой (например, у меня --failfast вообще не работал).

Причина кроется в сообщении из подобного DeprecationWarning'a:

/path/to/site-packages/django/test/simple.py:369: DeprecationWarning: The run_tests() test runner has been deprecated in favor of DjangoTestSuiteRunner.
  DeprecationWarning

Т.е., начиная с версии 1.3 мы должны забыть о функциях тест раннерах и использовать только классы тест раннеры. И так как готового класса тест раннера для django-celery еще нет, то нам прийдется руками разместить те настройки, которые устанавливает run_tests перед своим запуском, а именно:

CELERY_ALWAYS_EAGER = True
CELERY_EAGER_PROPAGATES_EXCEPTIONS = True

в test_settings модуль или куда-угодно еще. Ну и не забыть убрать djcelery.contrib.test_runner.run_tests из TEST_RUNNER.

5 июля 2011 г.

Считаем количество SQL запросов в Django тестах

Как-то раз мне понадобилось проверить, что после вызова определенной функции кол-во SQL запросов не изменилось. Просто? Конечно :)

from django.db import connection
from django.test import TestCase


class TestSomething(TestCase):

    def test_something(self):
        counter = len(connection.queries)
        something()
        self.assertEquals(len(connection.queries), counter)

И все бы ничего, но не реализовав необходимый функционал в something, я запустил тесты и с удивлением обнаружил, что они прошли чисто. Хм, не порядок, подумал я и добавил еще одну проверку:

self.assertNotEquals(len(connection.queries), counter)

Потом я запустил еще раз тесты и получил AssertionError: 0 == 0. Теперь все стало ясно, по дефолту в Django тестах нельзя посчитать количество SQL запросов, как len(connection.queries).

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

from django.conf import settings
from django.db import connection
from django.test import TestCase


class TestSomething(TestCase):

    def setUp(self):
        self.old_DEBUG = settings.DEBUG
        self.old_queries = connection.queries

        settings.DEBUG = True
        connection.queries = []

    def tearDown(self):
        settings.DEBUG = self.old_DEBUG
        connection.queries = self.old_queries

    def test_something(self):
        counter = len(connection.queries)
        something()
        self.assertEquals(len(connection.queries), counter)

И после запуска, тест упал как и ему было положено. Вот таким образом я смог посчитать кол-во запросов в Django тестах, чего и вам желаю!

зы. В комментариях подсказали более элегантный способ для подсчета количества SQL запросов в Django 1.3, просто используя метод assertNumQueries.

11 марта 2011 г.

Начальные пустые значения для forms.ChoiceField

Думаю, все знают, что список возможных значений forms.ModelChoiceField в дефолтном состоянии начинается с "сколько-то там тире". В то же время при моделировании значений для forms.ChoiceField также бывает необходимо добавить начальное пустое значение, причем не хочется делать это руками.

И правильно не хочется, ведь в Django уже есть заготовленные кортежи для этого:

>>> from django.db.models.fields import BLANK_CHOICE_DASH, BLANK_CHOICE_NONE
>>> BLANK_CHOICE_DASH
[('', '---------')]
>>> BLANK_CHOICE_NONE
[('', 'None')]

2 декабря 2010 г.

Вставляем виджет карты в Django шаблоны

Только что Mikhail Korobov зарелизил на PyPI незаменимую вещь для любого "корпоративного" сайта или сайта-визитки, разрабатываемого на Django, а именно приложение django-easy-maps позволяющее быстро и безболезненно вставлять виджет карты в Django шаблон.

Кратко о приложении, словами разработчика:

This app makes it easy to display a map for given address in django templates. No API keys, manual geocoding, html/js copy-pasting or django model changes is needed.

И краткий пример использования, взятый из README:

{% load easy_maps_tags %}

<!-- Default map with 300x400 dimensions -->
{% easy_map "Russia, Ekaterinburg, Mira 32" 300 400 %}

<!-- Variable address, custom detail level and custom template -->
{% easy_map address 200 200 5 using 'map.html' %}

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

25 ноября 2010 г.

Форматируем datetime.timedelta в что-то человеко-читаемое

Оговорюсь сразу, тем кому за глаза хватает print(datetime.timedelta(seconds=99660)) для форматирования и понимания содержимого datetime.timedelta могут смело не читать дальше.

Мне же приходится работать с datetime.timedelta очень часто и потому меня совершенно не устраивали встроенные возможности форматирования дельт. Чего мне не хватало больше всего так это вывода дельты в формате "G:i", говоря языком встроенного шаблонного фильтра date, т.е. показать общее кол-во часов и кол-во минут (не всех, а только остатка) для дельты. Именно для удовлетворения этих нужд я начал писать связку функций str_to_timedelta / timedelta_to_str, которые пару часов назад приняли окончательный вид.

Как они работают? Очень просто,

>>> import datetime
>>> from kikola.utils import str_to_timedelta, timedelta_to_str
>>> delta = datetime.timedelta(seconds=99660)
>>> timedelta_to_str(delta)
... u'27:41'
>>> timedelta_to_str(delta, 'd l, h:i:s')
... u'1 day, 03:41:00'
>>> timedelta_to_str(delta, 'f')
... u'1d 3:41'
>>> timedelta_to_str(delta, 'F')
... u'1 day, 3:41'
>>> timedelta_to_str(delta, 'S')
... u'99660'
>>> str_to_timedelta('27:41') == delta
... True
>>> timedelta_to_str('1 day, 03:41:00', 'd l, h:i:s') == delta
... True
>>> str_to_timedelta('1d 3:41') == delta
... True
>>> str_to_timedelta('1 day, 3:41') == delta
... True
>>> str_to_timedelta('99660', 'S') == delta
... True

Как видите, все действительно просто. Плюс к тому, если вам нужно использовать эти функции в шаблонах, вы можете воспользоваться фильтром timedelta из шаблонной библиотеки timedelta_tags.

{% load timedelta_tags %}
{{ delta|timedelta:"G:i:s" }}

Весь этот код довольно успешно живет в моем проекте kikola, полностью покрыт тестами и работает с Django 1.0+. Чтобы окончательно не углубляться, скажу только, что вы можете посмотреть все доступные форматы для форматирования в комментариях к timedelta_to_str, а также при частом использовании формата, отличного от дефолтного "G:i" вы можете указать его в TIMEDELTA_FORMAT переменной в настройках проекта, и затем str_to_timedelta и timedelta_to_str будут использовать указанный формат, как формат по умолчанию.

23 ноября 2010 г.

Тесты в Django - это быстро!

Одним из фактов, которым любят злоупотреблять люди, не желающие писать тесты, является то, что тесты - это медленно. И не только медленно писать сами тесты, но и медленно их запускать. И надо признаться Django раньше с удовольствием шла на поводу у этого факта.

Судите сами, у меня есть простой тестовый проект для своей библиотеки. В нем 39 тестов, из них 38 тестов по базе данных и 1 тест по работе с django.test.Client. В тестовом проекте есть 12 приложений, добавленных в INSTALLED_APPS, эти приложения создают 11 таблиц в базе данных и 3 индекса. Данные с фикстур нигде не загружаются. Никаких стресс-тестов проект не содержит.

Теперь давайте посмотрим на скорость выполнения тестов.

Django 1.0.4 справляется за ~14.8с, т.е. около 0.38c на каждый тест. И это действительно МЕДЛЕННО! Ведь повторюсь эти 39 тестов очень легкие в плане потребления ресурсов и очень щадящие к базе данных. Благо что 1.0.4 версию уже мало кто использует (я надеюсь) и потому этот результат не будет очень важным для нас.

Намного интересней и важней результат выполнения тестов в виртуальном окружении с Django 1.1.2. И он весьма неплох. Теперь 39 тестов успешно проходят всего за ~1.2с, т.е. более 10-кратное ускорение по сравнению с предыдущей версией Django. И теперь одному тесту требуется всего ничего, 0.03с. Неплохо, неплохо и главное не медленно. Но может ли быть быстрее?

В надеждах, что может быть и быстрее, я активирую виртуальное окружение с Django 1.2.3. И результаты выполнения тестов подтверждают мое предположение. Действительно еще быстрее. Теперь проход всех тестов занимает ~0.68с, практически в два раза быстрее предыдущей версии. Замечательно. И смело можно говорить, что тесты в Django - это быстро.

На сим можно было и заканчивать мой рассказ, но ведь я помню, что Alex Gaynor совсем недавно отрапортовал о еще более значительном ускорении выполнение тестов. Думая, куда ж еще быстрее, я все же активировал виртуальное окружение с версией Django из trunk-а и запустил выполнение тестов. К сожалению, а может и к радости, результаты не оказались какими-то заоблачными. Мои 39 тестов теперь проходят на ~0.08с быстрее и если это незначительный прирост относительно последней версии, то по сравнению даже с версией 1.1 - это около трех "бесплатных" теста.

А мораль сей басни такова, разработчики Django предостовляют в ваше разпоряжение по-настоящему быстрый инструмент для проведения тестирования, так что пользуйтесь ним, не забиывайте!

зы. Все тестовые классы в проекте наследовались от django.test.TestCase, тесты запускались как python testproject/manage.py test. Все стандартно.

24 апреля 2009 г.

Самое нужное из связки GSoC 2009 и Django

Как мне представляется - это будет проект Implementation of additional i18n features on Django.

Сегодня, Marc Garcia объяснил, что именно он хочет сделать в рамках этой программы. Меня впечатлило, если честно.

18 марта 2009 г.

Документирование django reusable apps при помощи Sphinx

Meio Código на основе статьи Rodrigo Lazo рассказывает, как использовать Sphinx для документирования django reusable (pluggable) apps. Особое внимание я прошу уделить на первый комментарий к записи Meio Código.

24 февраля 2009 г.

opentodo - Система управления задачами на Django

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

 Я, вообщем, буду пристально следить за развитием проекта. Так как он мне очень понравился ;) Возможно даже, я возьму себе его на вооружение для ведения своих проектов, код которых, я потом лью на Гитхаб.

 Демо-версия Страница на Google Code