Використання домішок (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. Дякую за увагу!


"Системні" налаштування 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

### ...