Principios y Cómo Escribir Decoradores Personalizados de Django

Un decorador es una herramienta poderosa en Django que permite agregar fácilmente lógica común a funciones o clases de vista. En este artículo, explicaré el principio de funcionamiento y cómo escribir decoradores paso a paso, para que puedas crear tu propio decorador personalizado.

1. ¿Qué es un decorador?

Un decorador es una función de orden superior en Python. Es decir, es una función que toma otra función como argumento y devuelve una nueva función dentro de ella.

  • Funciones de los decoradores:
    • Puede envolver una función para agregar o modificar características.
    • Facilita la reutilización y concisión del código.

2. Principio básico de funcionamiento del decorador

Para entender el funcionamiento de un decorador, veamos un ejemplo simple.

def simple_decorator(func):
    def wrapper():
        print("Antes de que se ejecute la función")
        func()
        print("Después de que se ejecute la función")
    return wrapper

@simple_decorator
def say_hello():
    print("¡Hola!")

say_hello()

Resultado de la salida:

Antes de que se ejecute la función
¡Hola!
Después de que se ejecute la función

Explicación:

  1. @simple_decorator pasa la función say_hello al decorador simple_decorator.
  2. simple_decorator crea y devuelve una nueva función wrapper.
  3. Cuando se llama a say_hello, se ejecuta wrapper en lugar de la función original.
  4. Dentro de wrapper, se ejecutan comportamientos adicionales (mensajes de impresión) y se llama a la función original.

3. Principios de escritura de decoradores personalizados en Django

Para escribir un decorador en Django, es necesario manejar el objeto request que es un argumento de las funciones de vista. Con esto se puede manipular la respuesta HTTP o ejecutar lógica según ciertas condiciones.

4. Estructura paso a paso para escribir un decorador personalizado

A continuación, se muestra la estructura básica para escribir un decorador personalizado en Django:

  1. Uso de functools.wraps: Se usa @wraps para mantener los metadatos de la función original, como el nombre y la cadena de documentación.
  2. Agregar lógica basada en request: Escribe lógica para analizar o validar el objeto de solicitud.
  3. Llamar a la función original o devolver una respuesta alternativa: Según ciertas condiciones, se puede bloquear la llamada a la función original y devolver una respuesta diferente.

A continuación se muestra el código de ejemplo:

from functools import wraps
from django.http import HttpResponseForbidden

def custom_decorator(view_func):
    @wraps(view_func)
    def _wrapped_view(request, *args, **kwargs):
        # Verificación de usuario en el objeto de solicitud
        if not request.user.is_authenticated:
            return HttpResponseForbidden("Debes estar logueado para acceder a esta página.")
        
        # Llamar a la función de vista original
        return view_func(request, *args, **kwargs)
    
    return _wrapped_view

Una ilustración visualmente atractiva que explica cómo funciona un decorador personalizado en Django

5. ¿Cómo agregar parámetros a un decorador?

Si un decorador necesita parámetros, se debe utilizar una estructura que envuelva la función dos veces. Esto se hace porque la primera función se utiliza para manejar los parámetros del decorador, mientras que la segunda función es la que envuelve la función de vista real.

Estructura básica:

def decorator_with_params(param1, param2):
    def actual_decorator(view_func):
        @wraps(view_func)
        def _wrapped_view(request, *args, **kwargs):
            # Lógica utilizando los parámetros param1, param2
            return view_func(request, *args, **kwargs)
        return _wrapped_view
    return actual_decorator

Explicación:

  1. La primera función decorator_with_params maneja los parámetros del decorador (param1, param2).
  2. Devuelve un decorador interno que envuelve la función de vista real (actual_decorator).
  3. Dentro de actual_decorator se llama a la función de vista original o se ejecuta la lógica adicional.

6. Ejemplo en la práctica: Decorador de acceso restringido por horarios

A continuación, se presenta un ejemplo de decorador que solo permite el acceso en ciertos horarios. Este decorador recibe como parámetros los horarios permitidos.

from datetime import datetime
from django.http import HttpResponseForbidden

def time_restricted_access(start_hour, end_hour):
    def decorator(view_func):
        @wraps(view_func)
        def _wrapped_view(request, *args, **kwargs):
            current_hour = datetime.now().hour
            if not (start_hour <= current_hour < end_hour):
                return HttpResponseForbidden("Esta página solo es accesible durante ciertas horas.")
            return view_func(request, *args, **kwargs)
        return _wrapped_view
    return decorator

@time_restricted_access(9, 17)  # Permitido de 9 AM a 5 PM
def working_hours_view(request):
    return render(request, 'working_hours.html')

Descripción del principio de funcionamiento:

  1. time_restricted_access es la primera función que maneja los parámetros (start_hour, end_hour).
  2. La primera función devuelve un decorador interno que envuelve la vista.
  3. El decorador recibe el request, comprueba la hora actual (datetime.now().hour) y según la condición temporal:
    • Si se satisface la condición, llama a la vista y devuelve el resultado.
    • Si no se satisface la condición, lo bloquea con HttpResponseForbidden.

¿Por qué envolver la función dos veces?

El motivo de la doble envoltura es separar las responsabilidades de manejo de parámetros y la función de vista.

  • Primera función: recibe los parámetros (start_hour, end_hour) y genera el decorador interno.
  • Segunda función: el decorador generado envuelve la función de vista y opera sobre el request.

Esta estructura hace que el decorador sea flexible y reutilizable.

7. Usar en vistas basadas en clases (CBV)

Para las vistas basadas en clases, es necesario utilizar method_decorator.

from django.utils.decorators import method_decorator
from django.views import View

@method_decorator(time_restricted_access(9, 17), name='dispatch')
class MyView(View):
    def get(self, request):
        return render(request, 'my_template.html')

8. Conclusión: Consideraciones al escribir decoradores personalizados

  1. Mantener la simplicidad: La lógica del decorador debe ser simple y clara.
  2. Considerar la reutilización: Si cierta lógica se utiliza en múltiples vistas, extráela en un decorador.
  3. Manejo de parámetros: Usa la estructura de doble envoltura cuando necesites parámetros.
  4. Preservación de metadatos: Utiliza @wraps para mantener los metadatos de la función original.

Conclusión

Ahora entiendes el principio de funcionamiento interno de los decoradores personalizados de Django y cómo escribir decoradores que acepten parámetros. ¡Utiliza este artículo como base para crear decoradores que se ajusten a tu proyecto!