Uso de Django Signals para simplificar y desacoplar el código

U

Introducción

Los sistemas se vuelven más complejos a medida que pasa el tiempo y esto justifica la necesidad de desacoplar más los sistemas. Un sistema desacoplado es más fácil de construir, extender y mantener a largo plazo, ya que el desacoplamiento no solo reduce la complejidad del sistema, sino que cada parte del sistema se puede administrar individualmente. La tolerancia a fallas también ha mejorado ya que, en un sistema desacoplado, un componente defectuoso no arrastra todo el sistema con él.

Django es un poderoso marco web de código abierto que se puede usar para construir sistemas grandes y complejos, así como pequeños. Sigue el patrón arquitectónico modelo-plantilla-vista y es fiel a su objetivo de ayudar a los desarrolladores a lograr la entrega de aplicaciones basadas en web basadas en datos complejos.

Django nos permite desacoplar la funcionalidad del sistema creando aplicaciones separadas dentro de un proyecto. Por ejemplo, podemos tener un sistema de compras y tener aplicaciones separadas que manejen cuentas, envío de recibos por correo electrónico y notificaciones, entre otras cosas.

En tal sistema, es posible que se necesiten varias aplicaciones para realizar una acción cuando ocurren ciertos eventos. Un evento puede ocurrir cuando un cliente realiza un pedido. Por ejemplo, necesitaremos notificar al usuario por correo electrónico y también enviar el pedido al proveedor o vendedor, al mismo tiempo podremos recibir y procesar pagos. Todos estos eventos suceden al mismo tiempo y, dado que nuestra aplicación está desacoplada, necesitamos mantener todos los componentes sincronizados, pero ¿cómo lo logramos?

Las señales de Django son útiles en tal situación, todo lo que debe suceder es que se envíe una señal cuando un usuario realiza un pedido, y cada componente relacionado o afectado lo escucha y realiza sus operaciones. Exploremos más sobre las señales en esta publicación.

Señales de un vistazo

Las señales de Django son una implementación del patrón Observer . En tal patrón de diseño, se implementa un mecanismo de suscripción donde múltiples objetos están suscritos u “observando” un objeto en particular y cualquier evento que pueda ocurrirle. Una buena analogía es cómo todos los suscriptores de un canal de YouTube reciben una notificación cuando un creador de contenido carga contenido nuevo.

A través de un “despachador de señales”, Django puede distribuir señales en una configuración desacoplada a los “receptores” registrados en los diversos componentes del sistema. Las señales se registran y activan cada vez que ocurren ciertos eventos, y cualquier oyente de ese evento será notificado de que el evento ha ocurrido, además de recibir algunos datos contextuales dentro de la carga útil que pueden ser relevantes para la funcionalidad del receptor. Un receptor puede ser cualquier función o método de Python. Más sobre esto más adelante.

Aparte del despachador de señales, Django también incluye algunas señales útiles que podemos escuchar. Incluyen:

  • post_save, que se envía cada vez que se crea y guarda un nuevo modelo de Django. Por ejemplo, cuando un usuario se registra o sube una publicación nueva,
  • pre_delete, que se envía justo antes de eliminar un modelo de Django. Un buen escenario sería cuando un usuario está eliminando un mensaje o su cuenta,
  • request_finished, que se activa cada vez que Django completa el servicio de una solicitud HTTP. Esto puede variar desde abrir el sitio web o acceder a un recurso en particular.

Otra ventaja de Django es que es un marco altamente personalizable. En nuestro caso, podemos crear nuestras señales personalizadas y utilizar el sistema integrado para enviarlas y recibirlas en nuestro sistema desacoplado. En la sección de demostración, nos suscribiremos a algunas de las señales integradas de Django y también crearemos algunas personalizadas propias.

Pero primero, veamos un ejemplo rápido que utiliza Django Signals. Aquí tenemos dos funciones que juegan ping-pong entre sí, pero interactúan a través de señales:

from django.dispatch import Signal, receiver

# Create a custom signal
ping_signal = Signal(providing_args=["context"])

class SignalDemo(object):
    # function to send the signal
    def ping(self):
        print('PING')
        ping_signal.send(sender=self.__class__, PING=True)

# Function to receive the signal
@receiver(ping_signal)
def pong(**kwargs):
    if kwargs['PING']:
        print('PONG')

demo = SignalDemo()
demo.ping()

En este sencillo script hemos creado una clase con un método para enviar la señal y una función separada fuera de la clase que recibirá y responderá. En nuestro caso, el emisor de la señal enviará el PINGcomando junto con la señal, y la función del receptor comprobará si el PINGcomando está presente e imprimirá PONGcomo respuesta. La señal se crea con la Signalclase de Django y es recibida por cualquier función que tenga el @receiverdecorador.

La salida del script:

$ python signal_demo.py

PING
PONG

Normalmente, tendríamos que invocar la pong()función desde dentro de la ping()función, pero con señales podemos obtener una solución similar pero desacoplada. La pong()función ahora puede residir en otro proyecto de archivo y aún responder a nuestra PINGseñal.

Cuándo usar señales

Ya hemos identificado qué son las señales de Django y cómo funcionan, pero como ocurre con cualquier otra característica del marco, no está destinado a ser utilizado en todo momento. Hay escenarios particulares en los que se recomienda encarecidamente que usemos señales de Django, e incluyen:

  • Cuando tenemos muchas piezas de código separadas interesadas en los mismos eventos, una señal ayudaría a distribuir la notificación del evento en lugar de que invoquemos todas las diferentes piezas de código en el mismo punto, lo que puede desordenarse e introducir errores.
  • También podemos usar señales de Django para manejar interacciones entre componentes en un sistema desacoplado como alternativa a la interacción a través de mecanismos de comunicación RESTful
  • Las señales también son útiles cuando se extienden bibliotecas de terceros donde queremos evitar modificarlas, pero necesitamos agregar funcionalidad adicional

Ventajas de las señales

Django Signals simplifica la implementación de nuestros sistemas desacoplados de varias formas. Nos ayudan a implementar aplicaciones reutilizables y, en lugar de volver a implementar la funcionalidad de forma individual, o modificar otras partes del sistema, podemos simplemente responder a las señales sin afectar a otros códigos. De esta manera, los componentes de un sistema se pueden modificar, agregar o eliminar sin tocar la base de código existente.

Las señales también proporcionan un mecanismo simplificado para mantener los diferentes componentes de un sistema desacoplado sincronizados y actualizados entre sí.

Proyecto de demostración

En nuestro proyecto de demostración, crearemos un tablero de trabajos simple donde los usuarios accederán al sitio, verán los trabajos disponibles y elegirán una publicación de trabajo para suscribirse. Los usuarios se suscribirán simplemente enviando su dirección de correo electrónico y serán notificados de cualquier cambio en el trabajo. Por ejemplo, si los requisitos cambian, la vacante se cierra o si se elimina la oferta de trabajo. Todos estos cambios serán realizados por un administrador que tendrá un panel para crear, actualizar e incluso eliminar las ofertas de trabajo.

Con el ánimo de desvincular nuestra aplicación, crearemos la aplicación principal de Jobs Board y una aplicación de notificaciones separada que tendrá la tarea de notificar a los usuarios cuando sea necesario. Luego usaremos señales para invocar la funcionalidad en la aplicación Notificaciones desde la aplicación principal de Jobs Board.

Otro testimonio del amplio conjunto de funciones de Django es el panel de administración integrado que nuestros administradores usarán para administrar los trabajos. Nuestro trabajo en ese frente se reduce considerablemente y podemos crear prototipos de nuestra aplicación más rápido.

Configuración del proyecto

Es una buena práctica construir proyectos de Python en un entorno virtual para que trabajemos en un entorno aislado que no afecte la configuración de Python del sistema, por lo que usaremos Pipenv.

Primero configuremos nuestro entorno:

# Set up the environment
$ pipenv install --three

# Activate the virtual environment
$ pipenv shell

# Install Django
$ pipenv install django

Django viene con algunos comandos que nos ayudan a realizar diversas tareas como crear un proyecto, crear aplicaciones, migrar datos y probar código, entre otras. Para crear nuestro proyecto:

# Create the project
$ django-admin startproject jobs_board && cd jobs_board

# Create the decoupled applications
$ django-admin startapp jobs_board_main
$ django-admin startapp jobs_board_notifications

Los comandos anteriores crearán un proyecto Django con dos aplicaciones dentro de él, que están desacopladas entre sí pero que aún pueden funcionar juntas. Para confirmar que nuestra configuración fue exitosa, migremos las migraciones predeterminadas que vienen con Django y configuremos nuestra base de datos y tablas:

$ python manage.py migrate
$ python manage.py runserver

Cuando accedemos a la instancia de ejecución local de nuestro proyecto Django, deberíamos ver lo siguiente:

Esto significa que hemos configurado nuestro proyecto Django con éxito y ahora podemos comenzar a implementar nuestra lógica.

Implementación

Django se basa en un patrón de arquitectura modelo-vista-plantilla, y este patrón también guiará nuestra implementación. Crearemos modelos para definir nuestros datos, luego implementaremos vistas para manejar el acceso y manipulación de datos y finalmente plantillas para mostrar nuestros datos al usuario final en el navegador.

Para tener nuestras aplicaciones integradas en la aplicación principal de Django, tenemos que agregarlas jobs_board/settings.pydebajo INSTALLED_APPS, de la siguiente manera:

INSTALLED_APPS = [
    # Existing apps remain...

    # jobs_board apps
    'jobs_board_main',
    'jobs_board_notifications',
]

Parte 1: La aplicación Main Jobs Board

Aquí es donde residirá la mayor parte de la funcionalidad de nuestro sistema y será el punto de interacción con nuestros usuarios. Contendrá nuestros modelos, vistas y plantillas y algunas señales personalizadas que usaremos para interactuar con la aplicación Notificaciones.

Comencemos creando nuestros modelos en jobs_board_main/models.py:

# jobs_board_main/models.py

class Job(models.Model):
    company = models.CharField(max_length=255, blank=False)
    company_email = models.CharField(max_length=255, blank=False)
    title = models.CharField(max_length=255, blank=False)
    details = models.CharField(max_length=255, blank=True)
    status = models.BooleanField(default=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

class Subscriber(models.Model):
    email = models.CharField(max_length=255, blank=False, unique=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

class Subscription(models.Model):
    email = models.CharField(max_length=255, blank=False, unique=True)
    user = models.ForeignKey(Subscriber, related_name="subscriptions", on_delete=models.CASCADE)
    job = models.ForeignKey(Job, related_name="jobs", on_delete=models.CASCADE)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

Creamos un modelo para definir nuestra Jobpublicación, que solo tendrá el nombre de la empresa y los detalles del trabajo junto con el estado de la oferta de trabajo. También tendremos un modelo para almacenar nuestros suscriptores tomando solo sus direcciones de correo electrónico. Los suscriptores y los trabajos se unen a través del Subscriptionmodelo donde almacenaremos los detalles de las suscripciones a las ofertas de trabajo.

Con nuestros modelos en su lugar, necesitamos realizar migraciones y migrarlos para tener las tablas creadas en la base de datos:

$ python manage.py makemigrations
$ python manage.py migrate

A continuación, pasamos a la sección de vista de nuestra aplicación. Creemos una vista para mostrar todas las ofertas de trabajo y otra para mostrar las ofertas de trabajo individuales donde los usuarios pueden suscribirse enviando sus correos electrónicos.

Comenzaremos creando la vista que manejará la visualización de todos nuestros trabajos:

# jobs_board_main/views.py

from .models import Job

def get_jobs(request):
    # get all jobs from the DB
    jobs = Job.objects.all()
    return render(request, 'jobs.html', {'jobs': jobs})

Para este proyecto usaremos vistas basadas en funciones, siendo la alternativa las vistas basadas en clases, pero eso no es parte de esta discusión. Consultamos la base de datos para todos los trabajos y respondemos a la solicitud especificando la plantilla que representará los trabajos y también incluyendo los trabajos en la respuesta.

Django viene con el motor de plantillas Jinja que usaremos para crear los archivos HTML que serán presentados al usuario final. En nuestra jobs_board_mainaplicación, crearemos una templatescarpeta que albergará todos los archivos HTML que presentaremos a los usuarios finales.

La plantilla para representar todos los trabajos mostrará todos los trabajos con enlaces a publicaciones de trabajos individuales, de la siguiente manera:

<!-- jobs_board_main/templates/jobs.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Jobs Board Homepage</title>
  </head>
  <body>
    <h2> Welcome to the Jobs board </h2>

    {% for job in jobs %}
      <div>
        <a href="/jobs/{{ job.id }}">{{ job.title }} at {{ job.company }}</a>
        <p>
          {{ job.details }}
        </p>
      </div>
    {% endfor %}

  </body>
</html>

Hemos creado el Jobmodelo, la get_jobsvista para obtener y mostrar todas las vistas y la plantilla para representar la lista de trabajos. Para unir todo este trabajo, tenemos que crear un punto final desde el cual los trabajos serán accesibles, y lo hacemos creando un urls.pyarchivo en nuestro jobs_board_main_application:

# jobs_board_main/urls.py

from django.urls import path
from .views import get_jobs

urlpatterns = [
    # All jobs
    path('jobs/', get_jobs, name="jobs_view"),
]

En este archivo, importamos nuestra vista, creamos una ruta y le adjuntamos nuestra vista. Ahora registraremos las URL de nuestras aplicaciones en el urls.pyarchivo principal de la jobs_boardcarpeta del proyecto:

# jobs_board/urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('jobs_board_main.urls')), # <--- Add this line
]

Nuestro proyecto está listo para ser probado ahora. Esto es lo que obtenemos cuando ejecutamos la aplicación y navegamos a localhost:8000/jobs:

Actualmente no tenemos puestos de trabajo. Django viene con una aplicación de administración que podemos usar para realizar nuestra entrada de datos. Primero, comenzamos creando un superusuario:

Con el superusuario creado, necesitamos registrar nuestros modelos en el admin.pyarchivo de nuestra jobs_board_mainaplicación:

# jobs_board_main/admin.py
from django.contrib import admin
from .models import Job

# Register your models here.
admin.site.register(Job)

Reiniciamos nuestra aplicación y navegamos localhost:8000/admine iniciamos sesión con las credenciales que acabamos de configurar. Este es el resultado:

Cuando hacemos clic en el signo más en la fila “Trabajos”, obtenemos un formulario en el que completamos los detalles sobre nuestra publicación de trabajo:

Cuando guardamos el trabajo y navegamos de regreso al jobspunto final, somos recibidos por la publicación de trabajo que acabamos de crear:

Ahora crearemos las vistas, las plantillas y las URL para mostrar un solo trabajo y también permitiremos que los usuarios se suscriban enviando su correo electrónico.

Nuestro jobs_board_main/views.pyse ampliará de la siguiente manera:

# jobs_board_main/views.py
# previous code remains
def get_job(request, id):
    job = Job.objects.get(pk=id)
    return render(request, 'job.html', {'job': job})

def subscribe(request, id):
    job = Job.objects.get(pk=id)
    sub = Subscriber(email=request.POST['email'])
    sub.save()

    subscription = Subscription(user=sub, job=job)
    subscription.save()

    payload = {
      'job': job,
      'email': request.POST['email']
    }
    return render(request, 'subscribed.html', {'payload': payload})

También necesitaremos crear la plantilla para una vista única de una publicación de trabajo en templates/job.html, que incluye el formulario que tomará el correo electrónico de un usuario y lo suscribirá a la publicación de trabajo:

<!-- jobs_board_main/templates/job.html -->
<html>
  <head>
    <title>Jobs Board - {{ job.title }}</title>
  </head>
  <body>
      <div>
        <h2>{{ job.title }} at {{ job.company }}</h2>
        <p>
          {{ job.details }}
        </p>
        <br>
        <p>Subscribe to this job posting by submitting your email</p>
        <form action="/jobs/{{ job.id }}/subscribe" method="POST">
          {% csrf_token %}
          <input type="email" name="email" id="email" placeholder="Enter your email"/>
          <input type="submit" value="Subscribe">
        </form>
        <hr>
      </div>
  </body>
</html>

Una vez que un usuario se suscribe a un trabajo, necesitaremos redirigirlo a una página de confirmación cuya subscribed.htmlplantilla será la siguiente:

<!-- jobs_board_main/templates/subscribed.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Jobs Board - Subscribed</title>
  </head>
  <body>
      <div>
        <h2>Subscription confirmed!</h2>
        <p>
          Dear {{ payload.email }}, thank you for subscribing to {{ payload.job.title }}
        </p>
      </div>
  </body>
</html>

Finalmente, nuestra nueva funcionalidad deberá exponerse a través de puntos finales que agregaremos a nuestro existente de la jobs_board_main/urls.pysiguiente manera:

# jobs_board_main/urls.py
from .views import get_jobs, get_job, subscribe

urlpatterns = [
    # All jobs
    path('jobs/', get_jobs, name="jobs_view"),
    path('jobs/<int:id>', get_job, name="job_view"),
    path('jobs/<int:id>/subscribe', subscribe, name="subscribe_view"),
]

Ahora podemos probar nuestra aplicación principal de Jobs Board al ver los listados de puestos de trabajo, hacer clic en uno y enviar una dirección de correo electrónico que recibirá actualizaciones.

Ahora que tenemos una aplicación en funcionamiento, es hora de incorporar Django Signals y notificar a los usuarios / suscriptores cuando se produzcan determinados eventos. Las ofertas de trabajo están vinculadas a una determinada empresa cuyo correo electrónico registramos, queremos notificarles cuando un nuevo usuario se suscribe a su oferta de trabajo. También queremos notificar a los usuarios suscritos cuando se elimine una oferta de trabajo.

Para notificar a los usuarios cuando una publicación de trabajo es eliminada o eliminada, utilizaremos la post_deleteseñal incorporada de Django . También crearemos nuestra señal llamada new_subscriberque usaremos para notificar a las empresas cuando los usuarios se suscriban a su oferta de trabajo.

Creamos nuestras señales personalizadas creando un signals.pyarchivo en nuestra jobs_board_mainaplicación:

# jobs_board_main/signals.py
from django.dispatch import Signal

new_subscriber = Signal(providing_args=["job", "subscriber"])

¡Eso es todo! Nuestra señal personalizada está lista para ser invocada después de que un usuario se haya suscrito exitosamente a una publicación de trabajo de la siguiente manera en nuestro jobs_board_main/views.pyarchivo:

# jobs_board_main/views.py

# Existing imports and code are maintained and truncated for brevity
from .signals import new_subscriber

def subscribe(request, id):
    job = Job.objects.get(pk=id)
    subscriber = Subscriber(email=request.POST['email'])
    subscriber.save()

    subscription = Subscription(user=subscriber, job=job, email=subscriber.email)
    subscription.save()

    # Add this line that sends our custom signal
    new_subscriber.send(sender=subscription, job=job, subscriber=subscriber)

    payload = {
      'job': job,
      'email': request.POST['email']
    }
    return render(request, 'subscribed.html', {'payload': payload})

No tenemos que preocuparnos por la pre_deleteseñal, ya que Django nos la enviará automáticamente justo antes de que se elimine una publicación de trabajo. La razón por la que estamos usando pre_deletey no post_deleteseñal es porque, cuando Jobse elimina a, todas las suscripciones vinculadas también se eliminan en el proceso y necesitamos esos datos antes de que también se eliminen.

Consumamos ahora las señales que acabamos de enviar en una jobs_board_notificationsaplicación separada .

Parte 2: La aplicación de notificaciones de Jobs Board

Ya creamos la jobs_board_notificationsaplicación y la conectamos a nuestro proyecto Django. En esta sección, consumiremos las señales enviadas desde nuestra aplicación principal y enviaremos las notificaciones. Django tiene una funcionalidad incorporada para enviar correos electrónicos, pero para fines de desarrollo, imprimiremos los mensajes en la consola.

Nuestra jobs_board_notificationsaplicación no necesita la interacción del usuario, por lo tanto, no necesitamos crear vistas o plantillas para ese propósito. Nuestro único objetivo es jobs_board_notificationsrecibir señales y enviar notificaciones. Implementaremos esta funcionalidad en nuestro, models.pyya que se importa temprano cuando se inicia la aplicación.

Recibamos nuestras señales en nuestro jobs_board_notifications/models.py:

# jobs_board_notifications/models.py.
from django.db.models.signals import pre_delete
from django.dispatch import receiver

from jobs_board_main.signals import new_subscriber
from jobs_board_main.models import Job, Subscriber, Subscription

@receiver(new_subscriber, sender=Subscription)
def handle_new_subscription(sender, **kwargs):
    subscriber = kwargs['subscriber']
    job = kwargs['job']

    message = """User {} has just subscribed to the Job {}.
    """.format(subscriber.email, job.title)

    print(message)

@receiver(pre_delete, sender=Job)
def handle_deleted_job_posting(**kwargs):
    job = kwargs['instance']

    # Find the subscribers list
    subscribers = Subscription.objects.filter(job=job)

    for subscriber in subscribers:
        message = """Dear {}, the job posting {} by {} has been taken down.
        """.format(subscriber.email, job.title, job.company)

        print(message)

En nuestro jobs_board_notifications, importamos nuestra señal personalizada, la pre_saveseñal y nuestros modelos. Usando el @receiverdecorador, capturamos las señales y los datos contextuales pasados ​​con ellos como argumentos de palabras clave.

Al recibir los datos contextuales, los usamos para enviar los “correos electrónicos” (recuerde que solo estamos imprimiendo en la consola para simplificar) a los suscriptores y empresas cuando un usuario se suscribe y se elimina una publicación de trabajo respondiendo al señales que enviamos.

Pruebas

Una vez que hayamos creado un trabajo en nuestro panel de administración, está disponible para que los usuarios se suscriban. Cuando los usuarios se suscriben, se envía el siguiente correo electrónico desde la jobs_board_notificationsaplicación a la empresa propietaria de la publicación:

Esta es una prueba de que nuestra new_subscriberseñal fue enviada desde la jobs_board_mainaplicación y recibida por la jobs_board_notificationsaplicación.

Cuando se elimina una publicación de trabajo, todos los usuarios que se suscribieron a la publicación de trabajo reciben una notificación por correo electrónico, de la siguiente manera:

La pre_deleteseñal de Django fue útil y nuestro administrador envió notificaciones a los usuarios suscritos de que se eliminó la publicación de trabajo en particular.

Resumen

En este artículo hemos construido un proyecto Django con dos aplicaciones que se comunican a través de Django Signals en respuesta a ciertos eventos. Nuestras dos aplicaciones están desacopladas y la complejidad en la comunicación entre nuestras aplicaciones se ha reducido considerablemente. Cuando un usuario se suscribe a una oferta de trabajo, notificamos a la empresa. A su vez, cuando se elimina una publicación de trabajo, notificamos a todos los clientes suscritos que la publicación de trabajo se eliminó.

Sin embargo, hay algunas cosas que debemos tener en cuenta al utilizar Django Signals. Cuando las señales no están bien documentadas, los nuevos encargados de mantenimiento pueden tener dificultades para identificar la causa raíz de ciertos problemas o comportamientos inesperados. Por lo tanto, cuando se utilizan señales en una aplicación, es una buena idea documentar las señales utilizadas, dónde se reciben y la razón detrás de ellas. Esto ayudará a cualquiera que mantenga el código a comprender el comportamiento de la aplicación y a resolver problemas más rápido y mejor. Además, es útil tener en cuenta que las señales se envían sincrónicamente. No se ejecutan en segundo plano ni mediante trabajos asincrónicos.

Con toda esta información sobre las señales de Django y el proyecto de demostración, deberíamos poder aprovechar el poder de las señales en nuestros proyectos web de Django.

El código fuente de este proyecto está disponible aquí en Github .

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad