Використання домішок (mix-in) у Django Class-based Views (CBV)

Нещодавно я повернувся до активного використання Django. І зразу до версії 1.5, в якій вже немає generic view у вигляді функцій, їх повністю витіснили Class Based Views. Спочтаку просто трохи розгубився, але коли дійшов до домішок (mix-in) став точнісінько як левеня у дуже влучному жарті від #soyeahdjango.

Me trying to figure out what a mix-in is

Me trying to figure out what a mix-in is

Тож давайте розберемося, що це за домішки і куди їх підмішують. Mixin — це підхід суть якого полягає у створенні класів, що реалізують допоміжну функціональність, яка може бути корисна об'єктам різних класів (не поєднаним спільним пращуром). Або іншими словами, додає функціональність, не втучаючись у "родовід", що дуже корисно, якщо класи зі сторонньої бібліотеки і внести зміни у верхніх щаблях ієрархії складно/неможливо. Такі класи-домішки потім використовуються при множинному наслідуванні, щоб надати додаткові властивості до успадкованих від батьківського.

Для того щоб розібратись з домішками на практиці, візьмемо таку задачу: перевірка чи є користувач власніком цього об'єкту при виконанні будь яких операцій. Декоратор permission_required для цього не підходить, а писати перевірку в кожному view окремо — суперечить принципу DRY.

Тренуватись будемо на двох моделях, пов'язаних зв'язком один до багатьох:

class Wishlist(models.Model):
    user = models.ForeignKey(User)
    name = models.CharField(max_length=300)
    private = models.BooleanField(default=True)


class WishlistItem(models.Model):
    wishlist = models.ForeignKey(Wishlist)
    name = models.CharField(max_length=300)
    link = models.URLField(max_length=1024, blank=True, null=True)
    note = models.TextField(blank=True, null=True)

Спочатку подивимось на набір view-ів для Wishlist:

class WishlistOwnershipCheckMixin(object):
    def get_object(self, *args, **kwargs):
        object = super(WishlistOwnershipCheckMixin, self).get_object(*args, **kwargs)
        if self.request.user != object.user:
            raise Http404
        return object


class WishlistDetailView(WishlistOwnershipCheckMixin, DetailView):
    model = Wishlist


class WishlistCreate(CreateView):
    form_class = WishlistForm
    model = Wishlist

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super(WishlistCreate, self).form_valid(form)


class WishlistUpdate(WishlistOwnershipCheckMixin, UpdateView):
    form_class = WishlistForm
    model = Wishlist


class WishlistDelete(WishlistOwnershipCheckMixin, DeleteView):
    model = Wishlist
    success_url = reverse_lazy('wishlists')

Клас WishlistOwnershipCheckMixin не має батьківсього класу, але має метод get_object(), який у свою чергу через super() викликає метод get_object() батьківсього класу. Тепер, замість того щоб перевизначати метод get_object() всім трьом класам (WishlistDetailView, WishlistUpdate, WishlistDelete), ми просто успадкоємо його від класу-домішки WishlistOwnershipCheckMixin.

Важливо!!! Пам'ятайте, що через певну плутанину з super(), домішку яка перевизначає метод класу з яким змішується потрібно вказувати першою у списку.

Ось так просто ми додали перевірку права власності до стандартних CBV. Але давайте подумаємо, чи можна використати нашу домішку для інших (як правило пов'язаних моделей), чи доведеться таки для кожної моделі і її CRUD view-ів писати свої домішки.

Якщо уважно подивитись на метод get_object(), то побачимо що у ньому є тільки одна жорстка залежність — наявність поля user у моделі з якою працюємо. WishlistItem пов'язаний з Wishlist, тож його user нам і потрібен. Найкращим способом додати його прямо до WishlistItem (щоб не звертатись до нього object.wishlist.user) мені здалося створення динамічного поля (property). Напишемо це з використанням lambda:

class WishlistItem(models.Model):
    """Wishist item model"""

    wishlist = models.ForeignKey(Wishlist)
    name = models.CharField(max_length=300)
    link = models.URLField(max_length=1024, blank=True, null=True)
    note = models.TextField(blank=True, null=True)
    user = property(lambda self: self.wishlist.user)

Тепер нашу домішку для перевірки права власності можна підмішати і до view-ів моделі WishlistItem:

class WishlistItemFormMixin(object):
    """
    Sets success_url to wishlist
    """
    def get_success_url(self):
        return reverse_lazy("wishlist", kwargs={'pk': self.object.wishlist.id, })


class WishlistItemCreate(WishlistItemFormMixin, CreateView):
    form_class = WishlistItemForm
    model = WishlistItem

    def dispatch(self, request, *args, **kwargs):
        self.wishlist = get_object_or_404(Wishlist, pk=self.kwargs['wishlist_pk'])
        if self.wishlist.user != request.user:
            raise Http404
        return super(WishlistItemCreate, self).dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        form.instance.wishlist = self.wishlist
        return super(WishlistItemCreate, self).form_valid(form)


class WishlistItemUpdate(WishlistItemFormMixin, WishlistOwnershipCheckMixin, UpdateView):
    form_class = WishlistItemForm
    model = WishlistItem


class WishlistItemDelete(WishlistItemFormMixin, WishlistOwnershipCheckMixin, DeleteView):
    model = WishlistItem

О! А ось і ще одна домішка — WishlistItemFormMixin, вона виправляє success_url (всього для двох view-ів, але то я на радостях, що освоїв домішки).

Ось така штука домішки, ось так їх можна використовувати у Django-вських CBV. Дякую за увагу!


Магія unicode у psycopg2

При написанні простенького веб-сервісу на Python/Django з доступом напряму до PostgreSQL, я зіткнувся з дивною поведінкою. Коли код запускався на тестовому сервері Django (manage.py runserver), psycopg2 вертав рядки як 'str' у кодуванні UTF-8. А коли запускався через WSGI під Apache2, то unicode. Я спочатку не вникав, чому так, але правити код на сервері після кожного оновлення, коли полізуть помилки, це не діло.

Маю підозру, що Django-вський сервер звертає увагу на # -*- coding: utf-8 -*-, тоді як mod_wsgi має це оголошення в носі, або навпаки.

Коли вже не знав що робити, як завжди допомогла документація. Там пишуть, що в Python 2 ви скоріш за все захочете завжди отримувати unicode. Так, я такий - хочу unicode. Для цього, одразу після імпорту psycopg2, задайте йому глобальні налаштування приведення типів, ось так:

import psycopg2

psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY)

І не сушіть собі більше голову, у вас завжди буде unicode :)


"Системні" налаштування Django-проекту

Цікавий підхід до організації файлу налаштувань Django-проекту (settings.py).

Виносимо певні змінні конфігурації "за дужки", може бути корисним для використання спільних опцій для кількох проектів або спрощення процедури оновлення проекту (локальні налаштування за межами каталогу проекту).

### settings.py

import os

ENVIRONMENT_SETTING_FILE = '/etc/django.myproject.settings'

### this will load all environment file settings in here

execfile(ENVIRONMENT_SETTING_FILE)

### all common settings

### ...

Іменування таблиць у базах даних

Стандартні рекомендації щодо іменування об'єктів БД застерігають від використання іменників у множині (наприклад, contracts замість contract). Але у вітчизняних (це я так завуальовано написав про "легасі" частину нашої БС) продуктах іноді зустрічаються назви у множині та ще й транслітеровані з української (наприклад, ugody замість contract). З такими назвами був цікавий казус: один з новачків з круглими від здивування очима спитав мене - що це за поле "угадано", хоча мався на увазі номер угоди "ugodano".

До чого я все це веду, ніколи не називайте об'єкти БД словами у множині і із застосуванням транслітерації, подивіться у словник і виберіть англійський відповідник слова, а якщо об'єкт і має якусь локальну специфіку, опишіть це в документації.