Pull to refresh

Кто создал, кто обновил или пишем своё встраиваемое приложение на django

Reading time 4 min
Views 12K
Как вы знаете, django очень мощный и гибкий фреймворк. Для него создано огромное количество приложений, как и каких-то личных так и публичных. Приложения могут быть как и достаточно монотонными, так и достаточно гибкими и даже встраиваемыми в другие приложения.

Задача


Нужно отслеживать кто создал и/или кто обновил данные в какой-нибудь модели.

Идея


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

Решение


Модели

Так как мы не знаем, изначально в какой модели будем использовать наш будующий функционал, то лучше всего добавлять его с помощью mixin’ов. Mixin — это класс, в котором содержится добавочный функционал для основного класса. В нашем случае это будут поля created_by, updated_by.

from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _

class CreatedByMixin(models.Model):
    created_by = models.ForeignKey(User, verbose_name=_('Created by'),
        related_name='%(class)s_created_items', 
    )
    
    class Meta:
        abstract=True

class UpdatedByMixin(models.Model):
    updated_by = models.ForeignKey(User, verbose_name=_('Updated by'),
        related_name='%(class)s_updated_items',
    )
    
    class Meta:
        abstract = True

class CreatedUpdatedByMixin(CreatedByMixin, UpdatedByMixin):
    class Meta:
        abstract = True


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

  • Как узнать кто создал/обновил запись?
  • Сигналы пишутся для определенных моделей. Как универсально написать их или сделать их максимально удобными для использования?


from django.db import models

from whovedonethis import models as who_models

class TestCreatedUpdated(who_models.CreatedUpdatedByMixin):
    value = models.CharField('Value', max_length=255)


Сохранение записи

Теперь есть поля в модели, но как записать туда того пользователя, который создал, обновил запись. Для этого есть несколько вариантов решений:

  • перегружать метод сохранение в форме для записи
  • добавлять пользователя при обработке во view
  • перегружать метод save() для модели
  • использовать сигналы.


Как мне кажется, наиболее универсальный способ это использование сигналов, так как в остальных случаях что-либо переписывается, или решение получается не универсальное.

Сигналы очень мощный инструмент, при помощи которого django позволяет реагировать на изменение системы( в нашем случае на создание или обновление записи в базе данных). Но при использовании их возникает некоторые проблемы:

  • Как узнать кто создал/обновил запись?
  • Сигналы пишутся для определенных моделей. Как универсально написать их или сделать их максимально удобными для использования?

Как узнать кто создал/обновил запись?


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

class Singleton(type):
    '''
        Singleton pattern requires for LoggedInUser class
    '''
    def __init__(cls, name, bases, dicts):
        cls.instance = None
    
    def __call__(cls, *args, **kwargs):
        if cls.instance is None:
            cls.instance = super(Singleton, cls).__call__(*args, **kwargs)
        return cls.instance 


class NotLoggedInUserException(Exception):
    '''
    '''
    def __init__(self, val='No users have been logged in'):
        self.val = val
        super(NotLoggedInUser, self).__init__()
    
    def __str__(self):
        return self.val

class LoggedInUser(object):
    __metaclass__ = Singleton
    
    user = None
    
    def set_user(self, request):
        if request.user.is_authenticated():
            self.user = request.user
    
    @property
    def current_user(self):
        '''
            Return current user or raise Exception
        '''
        if self.user is None:
            raise NotLoggedInUserException()
        return self.user

    @property
    def have_user(self):
	return not user is None

from whovedonethis.loggedinuser import LoggedInUser
class LoggedInUserMiddleware(object):
    '''
        Insert this middleware after django.contrib.auth.middleware.AuthenticationMiddleware
    '''
    def process_request(self, request):
        '''
            Returned None for continue request
        '''
        logged_in_user = LoggedInUser()
        logged_in_user.set_user(request)
        return None


Как удобнее использовать сигналы.

Так как сигналы это функции, то почему бы нам не написать декораторы, которые будут изменять поведение наших сигналов, добавляя нужный нам функционал. В данном случае это добавление пользователя в определённое поле. Так же давайте добавим проверку в какое поле надо сохранять пользователя, и если в модели задано поле ‘created_by_field/updated_by_field’, то сохранять будем его в заданное поле иначе в стандартное поле. Так как наш декоратор должен изменить только поведение функции, а не её структу, то будем использовать функцию wraps из functools.

from functools import wraps

from whovedonethis.loggedinuser import LoggedInUser

def add_created_user(f):
    '''
        Decorate pre_save signal for adding created user
    '''
    @wraps(f)
    def wrapper(sender, instance, **kwargs):
        if not instance.id:
            created_by_attr = getattr(instance, "created_by_field",
                "created_by"
            )
            setattr(instance, created_by_attr, LoggedInUser().current_user)
        return f(sender, instance, **kwargs)
    return wrapper

def add_updated_user(f):
    '''
        Decorate pre_save signal for adding created user
    '''
    @wraps(f)
    def wrapper(sender, instance, **kwargs):
        updated_by_attr = getattr(instance, "updated_by_field",
            "updated_by"
        )
        setattr(instance, updated_by_attr, LoggedInUser().current_user)
        return f(sender, instance, **kwargs)
    return wrapper
    
add_created_and_updated_user = lambda x: add_created_user(add_updated_user(x))
add_created_and_updated_user.__doc__ =\
    '''
        Decorate pre_save signal for adding created and updated user
    '''


Итог


Ну вот в принципе и всё. Теперь осталось всё собрать воедино и сделать наше встраиваемое приложение.

Репозитории:

  • с готовой библиотекой git://github.com/Zapix/whovedonethis.git
  • с примером работы этой библиотеки git://github.com/Zapix/test_whovedonethis.git

P.S.

Спасибо за замечания и указания ошибки, особенно в орфографии и пунктуации.

На написание данной статьи меня вдохновил доклад Кори Оордта с PyCon 2011.

UPD.
Пример использования сигнала с декоратором:

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

from whovedonethis import decorator

from testlib.models import TestCreated

@receiver(pre_save, sender=TestCreated)
@decorator.add_created_user
def pre_save_testcreated_handler(sender, instance, **kwargs):
    pass
Tags:
Hubs:
+32
Comments 12
Comments Comments 12

Articles