Перед нами на работе встала задача, ограничить частоту публикации комментариев. Реализовать решили через декоратор для вьюшек.
Другими словами нужно создать декоратор, не допускающий обращение к конкретной view, конкретным пользователем в течении определенного времени.
.clip {
display: block;
background: #EEE;
border: 1px dashed #999;
padding: 1em 1em 0 1em;
}
Декоратор — обертка для функции. С помощью декоратора можно изменять поведение декорируемой функции, ее входные или выходные параметры.
Примеры декораторов из Django:
- login_required
- при обращении анонимного пользователя к декорированому view, перенаправляет его на страницу логина.
- transaction.commit_on_success
- выполняет все запросы из декорируемой функции к БД в одной транзакции и коммитит ее при успешном завершении.
- cache_page(sec)
- кеширует результат выполнения view на sec секунд.
Два способа применения декоратора:
@login_required
def my_view(request):
...
def my_view(request):
...
my_view = login_required(my_view) #python 2.3
Получение id пользователя не вызывает проблем — request.user. А вот с получением имени функции не так все просто. Вот шаблон декоратора:
def decorator(function):
def actual_decorator(request, *args, **kwargs):
pass
return actual_decorator
То есть, мы имеем указатель на декорируемую функцию function, и можно получить ее имя через function.func_name. Но тут возникает проблема, к функции часто применяется последовательность декораторов, например:
@limit_rate_request
@login_required
def my_view(request):
...
В этой ситуации function будет ссылаться на декоратор login_required и имя декорируемой функции мы получить не можем.
Это внутренняя функция, содержащая ссылки на локальные переменные внешней функции.
Например:
def func1():
closure = 1
def func2():
print closure
В этом примере переменная closure замкнута в функции func2.
Так как на login_required замкнут указатель на декорируемую функцию, можно попробовать получить ее значение. У функции в питоне существует параметр func_closure — все замкнутые на функцию переменную. Но, к сожалению, их значения представлены классом cell и получить значение напрямую нельзя.
На помощь приходит Решение :)
def get_cell_value(cell):
# функция, которая возращает функцию, которая в свою очередь вернет замкнутую на нее переменную
def make_closure_that_returns_value(use_this_value):
def closure_that_returns_value():
return use_this_value
return closure_that_returns_value
# получаем экземпляр функции с замыканием с параметром 0
dummy_function = make_closure_that_returns_value(0)
# получаем байт-код этой функции
dummy_function_code = dummy_function.func_code
# создаем новый экземпляр функции вместо старой замкнутой переменной используем наш cell
our_function = new.function(dummy_function_code, {}, None, None, (cell,))
# вызываем функцию, и она возвращает значение нашей замкнутой переменной из cell
value_from_cell = our_function()
return value_from_cell
Далее дело за малым — создать декоратор. Для хранения времени последнего вызова достаточно удобно использовать кеш, установив время его жизни равным времени ограничения на запуск функции.
Таким образом, после первого запроса View в кеше будет установлена переменная PREFIX_USER_ID_FUNCTION_NAME, пока повторный запуск запрещен эта переменная хранится в кеше, как только таймаут вышел, кеш «протухает» и пользователь снова может сделать запрос.
Полный код декоратора:
def get_cell_value(cell):
def make_closure_that_returns_value(use_this_value):
def closure_that_returns_value():
return use_this_value
return closure_that_returns_value
dummy_function = make_closure_that_returns_value(0)
dummy_function_code = dummy_function.func_code
our_function = types.FunctionType(dummy_function_code, {}, None, None, (cell,))
value_from_cell = our_function()
return value_from_cell
def get_decorated_function(function):
""" Returns actual decorated function in decorators stack """
while function.func_closure is not None:
function = get_cell_value(function.func_closure[0])
return function
class limit_request_rate(object):
"""
Decorator for view that limit request rate for view for concrete user.
Anonymous users not limited.
@todo: differentiate anonymous users by IP
"""
CACHE_VAR_PREFIX = "limit_request_rate_"
def __init__(self, timeout = None):
self.timeout = timeout or settings.DEFAULT_REQUEST_TIMEOUT
def __call__(self, function):
def actual_decorator(request, *args, **kwargs):
if request.user.is_authenticated():
dec_func = get_decorated_function(function)
cache_key = self.CACHE_VAR_PREFIX + str(request.user.id) + dec_func
.func_name + dec_func.func_code.co_filename
if not cache.get(cache_key):
cache.set(cache_key, 1, self.timeout)
return function(request, *args, **kwargs)
else:
return HttpResponseServiceUnavailable()
return function(request, *args, **kwargs)
return actual_decorator
up: deprecated new.function заменен на Types.FunctionType
Оставьте комментарий, если вам есть что добавить, вы нашли ошибку в коде или что-то осталось непонятным.