lunes, 12 de febrero de 2024

27- Internacionalización con Django

 Internacionalización con Django

Django ofrece un completo soporte de internacionalización y localización. Te permite traducir tu aplicación a múltiples idiomas y manejar el formato específico para fechas, horas, números y zonas horarias según la configuración local. Vamos a aclarar la diferencia entre internacionalización y localización:

- Internacionalización (frecuentemente abreviada como i18n) es el proceso de adaptar el software para el potencial uso de diferentes idiomas y configuraciones locales, de modo que no esté fijado a un idioma o configuración local específica.

- Localización (abreviada como l10n) es el proceso de traducir el software y adaptarlo a una configuración local particular. Django mismo está traducido a más de 50 idiomas utilizando su marco de internacionalización.

El marco de internacionalización te permite marcar fácilmente cadenas para la traducción, tanto en el código Python como en tus plantillas. Se basa en la herramienta GNU gettext para generar y gestionar archivos de mensajes. Un archivo de mensaje es un archivo de texto plano que representa un idioma. Contiene una parte o todas las cadenas de traducción encontradas en tu aplicación y sus respectivas traducciones para un único idioma. Los archivos de mensaje tienen la extensión .po. Una vez que la traducción está hecha, los archivos de mensajes se compilan para ofrecer un acceso rápido a las cadenas traducidas. Los archivos de traducción compilados tienen la extensión .mo.


Configuración de internacionalización y localización


Django proporciona varios ajustes para la internacionalización. Los siguientes ajustes son los más relevantes:

- USE_I18N: Un booleano que especifica si el sistema de traducción de Django está habilitado. Esto es Verdadero por defecto.

- USE_L10N: Un booleano que indica si está habilitado el formato localizado. Cuando está activo, se utilizan formatos localizados para representar fechas y números. Esto es Falso por defecto.

- USE_TZ: Un booleano que especifica si las fechas y horas son conscientes de la zona horaria. Cuando creas un proyecto con el comando startproject, esto se establece en Verdadero.

- LANGUAGE_CODE: El código de idioma predeterminado para el proyecto. Está en el formato estándar de ID de idioma, por ejemplo, 'en-us' para inglés americano o 'en-gb' para inglés británico. Esta configuración requiere que USE_I18N esté establecido en Verdadero para que tenga efecto. Puedes encontrar una lista de ID de idiomas válidos en http://www.i18nguy.com/unicode/language-identifiers.html.

- LANGUAGES: Una tupla que contiene los idiomas disponibles para el proyecto. Vienen en dos tuplas de un código de idioma y un nombre de idioma. Puedes ver la lista de idiomas disponibles en django.conf.global_settings. Cuando elijas en qué idiomas estará disponible tu sitio, estableces LANGUAGES en un subconjunto de esa lista.

- LOCALE_PATHS: Una lista de directorios donde Django busca archivos de mensajes que contienen traducciones para el proyecto.

- TIME_ZONE: Una cadena que representa la zona horaria para el proyecto. Se establece en 'UTC' cuando creas un nuevo proyecto usando el comando startproject. Puedes establecerlo en cualquier otra zona horaria, como 'Europe/Madrid'.


Estos son algunos de los ajustes de internacionalización y localización disponibles. Puedes encontrar la lista completa en https://docs.djangoproject.com/en/5.0/ref/settings/#globalization-i18n-l10n.


Comandos de gestión de internacionalización


Django incluye los siguientes comandos de gestión para gestionar las traducciones:

- makemessages: Esto recorre el árbol de origen para encontrar todas las cadenas marcadas para traducción y crea o actualiza los archivos de mensajes .po en el directorio local. Se crea un único archivo .po para cada idioma.

- compilemessages: Esto compila los archivos de mensajes .po existentes en archivos .mo, que se utilizan para recuperar traducciones.


Instalación del kit de herramientas gettext


Necesitarás el kit de herramientas gettext para poder crear, actualizar y compilar archivos de mensajes. La mayoría de las distribuciones de Linux incluyen el kit de herramientas gettext. Si estás usando macOS, la forma más sencilla de instalarlo es a través de Homebrew, en https://brew.sh/, con el siguiente comando:

brew install gettext

También es posible que necesites vincularlo forzosamente con el siguiente comando:

brew link --force gettext

Si estás usando Windows, sigue los pasos en https://docs.djangoproject.com/en/5.0/topics/i18n/translation/#gettext-on-windows. Puedes descargar un instalador binario precompilado de gettext para Windows desde https://mlocati.github.io/articles/gettext-iconv-windows.html.


Cómo añadir traducciones a un proyecto Django


Veamos el proceso de internacionalizar tu proyecto. Necesitarás hacer lo siguiente:

1. Marcar las cadenas para traducción en tu código Python y en tus plantillas.

2. Ejecutar el comando makemessages para crear o actualizar archivos de mensajes que incluyan todas las cadenas de traducción de tu código.

3. Traducir las cadenas contenidas en los archivos de mensajes y compilarlos usando el comando de gestión compilemessages.


Cómo determina Django el idioma actual


Django viene con un middleware que determina el idioma actual basado en los datos de la solicitud. Este es el middleware LocaleMiddleware que reside en django.middleware.locale.LocaleMiddleware, que realiza las siguientes tareas:

1. Si estás usando i18n_patterns, es decir, estás usando patrones de URL traducidos, busca un prefijo de idioma en la URL solicitada para determinar el idioma actual.

2. Si no se encuentra ningún prefijo de idioma, busca un LANGUAGE_SESSION_KEY existente en la sesión del usuario actual.

3. Si el idioma no está configurado en la sesión, busca una cookie existente con el idioma actual. Se puede proporcionar un nombre personalizado para esta cookie en el ajuste LANGUAGE_COOKIE_NAME. Por defecto, el nombre de esta cookie es django_language.

4. Si no se encuentra ninguna cookie, busca en la cabecera Accept-Language de la solicitud.

5. Si la cabecera Accept-Language no especifica un idioma, Django utiliza el idioma definido en el ajuste LANGUAGE_CODE.


Por defecto, Django utilizará el idioma definido en el ajuste LANGUAGE_CODE a menos que estés usando LocaleMiddleware. El proceso descrito aquí solo se aplica al usar este middleware.


Preparando tu proyecto para la internacionalización


A continuación vamos a preparar nuestro proyecto para que se pueda usar diferentes lenguas. Vamos a crear una versión en Inglés y en Español de la tienda. Edita el archivo settings.py de la aplicación principal y añade la siguiente configuración al parámetro LANGUAGES. Pon lo al lado de la configuración LANGUAGE_CODE de esta forma:

PracticaDjango/PracticaDjango/settings.py

#...
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/

LANGUAGE_CODE = 'es'

LANGUAGES = [
    ('es', 'Español'),
    ('en', 'English'),
]

TIME_ZONE = 'Europe/Madrid'

USE_I18N = True

USE_TZ = True
La configuración LANGUAGES contiene dos tuplas que consisten en un código de idioma y un nombre. Los códigos de idioma pueden ser específicos de la localidad, como en-us o en-gb, o genéricos, como "en". Con esta configuración, especificas que tu aplicación solo estará disponible en inglés y español. Si no defines una configuración de LANGUAGES personalizada, el sitio estará disponible en todos los idiomas en los que Django esté traducido.

Haz que tu configuración LANGUAGE_CODE se vea como sigue:

LANGUAGE_CODE = 'es'

Agrega 'django.middleware.locale.LocaleMiddleware' a la configuración MIDDLEWARE. Asegúrate de que este middleware venga después de SessionMiddleware porque LocaleMiddleware necesita usar datos de sesión. También debe colocarse antes de CommonMiddleware porque este último necesita un idioma activo para resolver la URL solicitada. La configuración MIDDLEWARE debería verse ahora como sigue:

PracticaDjango/PracticaDjango/settings.py

#...
SITE_ID = 1  # define the site id

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'PracticaDjango.urls'
NOTA: El orden de las clases de middleware es muy importante porque cada middleware puede depender de los datos establecidos por otro middleware que se ejecutó previamente. El middleware se aplica a las solicitudes en el orden de aparición en MIDDLEWARE, y en orden inverso para las respuestas.

Crea la siguiente estructura de directorios dentro del directorio principal del proyecto, junto al archivo manage.py:

locale/
    es/
    en/
   
El directorio "locale" es el lugar donde residirán los archivos de mensajes para tu aplicación. Edita el archivo de configuración settings.py nuevamente y agrega la siguiente configuración debajo de LANGUAGES:


PracticaDjango/PracticaDjango/settings.py

LOCALE_PATHS = [
    BASE_DIR / 'locale',
]

La configuración LOCALE_PATHS especifica los directorios donde Django debe buscar archivos de traducción. Los directorios de localización que aparecen primero tienen la mayor precedencia.

Cuando uses el comando makemessages desde el directorio de tu proyecto, los archivos de mensajes se generarán en la ruta "locale/" que creaste. Sin embargo, para aplicaciones que contienen un directorio "locale/", los archivos de mensajes se generarán en ese directorio.

En resumen. Le estamos indicando a Django que utilice el LocaleMiddleware el cual se encargará de escoger el lenguaje adecuado para cada usuario basado en información extraída del request que normalmente será el idioma local del ordenador del usuario (Es importante ubicar este middleware entre SessionMiddleware y CommonMiddleware si estás usando ambos, de lo contrario podría no funcionar bien). También especificamos mediante la variable LANGUAGES qué lenguajes va a soportar nuestro proyecto y por último, especificamos la ruta de la carpeta en la cual se guardarán los archivos de traducción mediante la variable LOCALE_PATHS. 


Traduciendo código Python.


Para traducir literales en tu código Python, puedes marcar cadenas para la traducción usando la función gettext() incluida en django.utils.translation. Esta función traduce el mensaje y devuelve una cadena. La convención es importar esta función como un alias más corto llamado _ (el carácter de subrayado). Puedes encontrar toda la documentación sobre traducciones en https://docs.djangoproject.com/en/5.0/ topics/i18n/translation/.

Traducciones estándar

El siguiente código muestra cómo marcar una cadena para traducción:

from django.utils.translation import gettext as _
output = _('Texto a ser traducido.')

Traducciones Lazy


Django incluye versiones perezosas para todas sus funciones de traducción, que tienen el sufijo _lazy(). Cuando se usan las funciones lazy o perezosas, las cadenas se traducen cuando se accede al valor, en lugar de cuando se llama a la función (por eso se traducen de forma perezosa). Las funciones de traducción perezosa son útiles cuando las cadenas marcadas para traducción están en rutas que se ejecutan cuando se cargan los módulos.

Nota: Usar gettext_lazy() en lugar de gettext() significa que las cadenas se traducen cuando se accede al valor. Django ofrece una versión perezosa para todas las funciones de traducción.

Las traducciones que incluyen variables


Las cadenas marcadas para traducción pueden incluir marcadores de posición para incluir variables en las traducciones. El siguiente código es un ejemplo de una cadena de traducción con un marcador de posición:

from django.utils.translation import gettext as _
month = _('April')
day = '14'
output = _('Hoy es %(month)s %(day)s') % {'month': month, 'day': day}

Al usar marcadores de posición, puedes reordenar las variables de texto. Por ejemplo, una traducción al inglés del ejemplo anterior podría ser "Today is April 14", mientras que la traducción al español podría ser "hoy es 14 de abril". Siempre usa interpolación de cadenas en lugar de interpolación posicional cuando tengas más de un parámetro para la cadena de traducción. De esta manera, podrás reordenar el texto del marcador de posición.

Plurales en las traducciones.


Para formas plurales en las traducciones, puedes utilizar ngettext() y ngettext_lazy(). Estas funciones traducen formas singular y plural dependiendo de un argumento que indica el número de objetos. El siguiente ejemplo muestra cómo usarlas:

output = ngettext('hay %(count)d producto',
                  'hay %(count)d productos',
                  count) % {'count': count}

Ahora que conoces los conceptos básicos de traducción de literales en tu código Python, es hora de aplicar las traducciones a tu proyecto.

Traduciendo tu propio código.


Edita el archivo settings.py del proyecto, importa la función gettext_lazy y cambia la opción LANGUAGES de la siguiente forma para traducir el nombre de las lenguas.

PracticaDjango/PracticaDjango/settings.py

# Para la internacionalización del proyecto
from django.utils.translation import gettext_lazy as _

LANGUAGES = [
    ('es', _('Español')),
    ('en', _('English')),
]

Nota: Si en linux te da un error de que no se puede encontrar un determinado archivo vuelve a instalar gettext con sudo apt intstall gettext.

Aquí, vamos a usar la función gettext_lazy en vez de gettext para evitar una importación circular, con esto traduciremos los nombres de los lenguajes cuando acedamos a ellos.

Abre el shel y ejecuta el siguiente comando desde el directorio principal del proyecto donde esta manage.py:

$ django-admin makemessages --all
Al ejecutar este comando la salida debería ser:

processing locale en
processing locale es
Echa un vistazo al directorio que creamos antes, el directorio locale. Deberías ver una estructura de directorios como la siguiente:

en/
    LC_MESSAGES/
        django.po
es/
    LC_MESSAGES/
        django.po
Un archivo .po ha sido creado para cada lenguaje. Como nuestro código ha sido escrito en español, tendremos que hacer la traducción al ingles. Abre el archivo en/LC_MESSAGES/django.po con un editor de texto. Al final del archivo verás lo siguiente:

PracticaDjango/locale/en/LC_MESSAGES/django.po

#: PracticaDjango/settings.py:149
msgid "Español"
msgstr ""

#: PracticaDjango/settings.py:150
msgid "English"
msgstr ""
Cada cadena de traducción está precedida por un comentario que muestra detalles sobre el archivo y la línea donde fue encontrado. Cada traducción incluye dos cadenas:

• msgid: la cadena de traducción tal como aparece en el código fuente.
• msgstr: La traducción del idioma, que está vacía de forma predeterminada. Aquí es donde tienes que entrar la traducción real de la cadena dada.

Completa las traducciones de msgstr para la cadena msgid dada, de la siguiente manera:

PracticaDjango/locale/en/LC_MESSAGES/django.po

#: PracticaDjango/settings.py:149
msgid "Español"
msgstr "Spanish"

#: PracticaDjango/settings.py:150
msgid "English"
msgstr "English"
Guarda las modificaciones que hemos hecho en el archivo, abre el shell y ejecuta el siguiente comando:

$ django-admin compilemessages
Si todo ha ido bien deberías ver una salida del comando como esta:

processing locale en
processing locale es
Los archivos han sido compilados (.mo) y si observas el directorio "locale" verás ahora lo siguiente:

en/
    LC_MESSAGES/
        django.mo
        django.po
es/
    LC_MESSAGES/         django.mo        
        django.po
¡Enhorabuena! Has traducido los nombres de los idiomas. Ahora, traduzcamos los nombres de los campos un modelo de una aplicación del proyecto. Edita el archivo models.py de la aplicación "Servicios", por ejemplo, y vamos a añadir a los campos del modelo el etiquetado para su traducción de la siguiente manera:

PracticaDjango/Servicios/models.py

# Para internacionalizar el modelo
from django.utils.translation import gettext_lazy as _
#...
class Servicio(models.Model):
    titulo = models.CharField(_('titulo'),max_length=50)
    contenido = models.CharField(_('contenido'), max_length=50)
    imagen = models.ImageField(_('imagen'), upload_to='Servicios')
    created = models.DateField(auto_now_add=True)
    # Para que automaticamente actualice las fechas usamos el argumento
    # auto_now_add = True para guardar la fecha de cuando se cree el registro
    updated = models.DateField(auto_now=True)
    # auto_now = True para guardar la fecha cuando se guarde o actualice el 
    # registro.

Ha agregado nombres para los campos que se muestran cuando el administrador añada nuevos servicios que tenga nuestra tienda. Estos son titulo, contenido e imagen. Recuerda que también puedes utilizar
el atributo verbose_name para nombrar los campos.

Crea el siguiente directorio dentro de la aplicación Servicios:

locale/
    en/
    es/
Al crear un directorio local, las cadenas de traducción de esta aplicación se almacenarán en un archivo de mensajes .po, en este directorio en lugar del archivo de mensajes principal. De esta manera, podemos generar por separado archivos de traducción para cada aplicación.

Abra el shell desde el directorio del proyecto y ejecuta el siguiente comando:

$ django-admin makemessages --all
Salida:
processing locale es
processing locale en
Abra el archivo locale/en/LC_MESSAGES/django.po de la aplicación de Servicios usando un editor de texto. Verás las cadenas de traducción para el modelo de Servicios. Completa las siguientes traducciones msgstr para las cadenas dadas de msgid:

msgid "titulo"
msgstr "title"

#: Servicios/models.py:10
msgid "contenido"
msgstr "content"

#: Servicios/models.py:11
msgid "imagen"
msgstr "image"
Después de terminada la traducción, guarda el archivo. 

Además de un editor de texto, puedes utilizar Poedit para editar traducciones. Poedit es un software para editar traducciones que utiliza gettext. Está disponible para Linux, Windows y macOS. Puedes descargar Poedit desde https://poedit.net/.

También vamos a traducir los formularios del proyecto. Para ver un ejemplo vamos a traducir el formulario de contacto. Edita el archivo forms.py dentro del directorio de la aplicación de Contacto y agrega un atributo de etiqueta a los campos de la siguiente manera:

PracticaDjango/Contacto/forms.py

from django import forms

# Internacionalización
from django.utils.translation import gettext_lazy as _


class FormularioContacto(forms.Form):
    # Especificamos los campos del formulario
    nombre = forms.CharField(label=_("Nombre"), max_length=50)
    email = forms.EmailField(label=_("Email"), max_length=50)
    contenido = forms.CharField(
        label=_("Contenido"),
        max_length=400,
        widget=forms.Textarea(attrs={"cols": 45, "rows": 5}),
    )
Con esto hemos marcado el nombre de las etiquetas para su posterior traducción al inglés.

Traduciendo las plantillas.


Django ofrece las etiquetas de plantilla {% trans %} y {% blocktrans %} para traducir las cadenas en las plantillas. Para usar las etiquetas de plantilla de traducción, debes agregar {% load i18n %} en la parte superior de tu plantilla para cargarlas.

La etiqueta de plantilla {% trans %}

La etiqueta de plantilla {% trans %} te permite marcar un literal para su traducción. Internamente, Django ejecuta gettext() en el texto dado. Así es como se marca una cadena para traducción en una plantilla:

```
{% trans "Texto a ser traducido" %}
```

Puedes usar as para almacenar el contenido traducido en una variable que puedes utilizar en toda tu plantilla. El siguiente ejemplo almacena el texto traducido en una variable llamada saludo:

```
{% trans "¡Hola!" as saludo %}
<h1>{{ saludo }}</h1>
```

La etiqueta {% trans %} es útil para cadenas de traducción simples, pero no puede manejar contenido para traducción que incluya variables.

La etiqueta de plantilla {% blocktrans %}

La etiqueta de plantilla {% blocktrans %} te permite marcar contenido que incluye literales y contenido variable utilizando marcadores de posición. El siguiente ejemplo te muestra cómo usar la etiqueta {% blocktrans %}, incluyendo una variable de nombre en el contenido para traducción:

```
{% blocktrans %}¡Hola {{ nombre }}!{% endblocktrans %}
```

Puedes usar with para incluir expresiones de plantilla, como acceder a atributos de objetos o aplicar filtros de plantilla a variables. Siempre debes usar marcadores de posición para estas. No puedes acceder a expresiones o atributos de objetos dentro del bloque blocktrans. El siguiente ejemplo te muestra cómo usar with para incluir un atributo de objeto al que se ha aplicado el filtro capfirst:

```
{% blocktrans with nombre=user.nombre|capfirst %}
¡Hola {{ nombre }}!
{% endblocktrans %}
```

Traducir las plantillas de la Tienda


Edita la plantilla Tienda/tienda.html de la aplicación de la Tienda. Asegúrate de cargar la etiqueta i18n en la parte superior de la plantilla y marcar las cadenas para traducción, como se muestra a continuación. El código nuevo se resalta en azul:

PracticaDjango/Tienda/templates/Tienda/tienda.html

<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}
{% load thumbnail %}
{% load i18n %}

<!-- Establecemos el titulo de la página -->
{% block title %}{% trans "Tienda" %}{% endblock %}

<!-- Definimos su contenido -->
{% block content %}
<h1 class="text-center">{% trans "Elige tu Consola." %}</h1>

<div class="container">
  <div class="bg-dark">

Usa la etiqueta {% blocktrans %} en lugar de {% trans %} cuando necesites incluir contenido variable en tu cadena de traducción.

Asegúrate de que ninguna etiqueta de plantilla se divida en varias líneas.

Igual que con esta plantilla puedes hacerlo con el resto para traducirlas.

Vamos a actualizar los archivos de mensajes para incluir las nuevas cadenas de traducción. Abre la terminal y ejecuta el siguiente comando:

```
django-admin makemessages --all
```

Los archivos .po están dentro del directorio "locale" del proyecto "PracticaDjango", y verás que la aplicación de pedidos ahora contiene todas las cadenas que marcamos para su traducción.

Edita los archivos de traducción .po del proyecto y de la aplicación de Servicios e incluye las traducciones al inglés en msgstr. 

Ejecuta el siguiente comando para compilar los archivos de traducción:

```
django-admin compilemessages
```


Se ha generado un archivo .mo que contiene las traducciones compiladas para cada archivo .po de traducción.


Utilizando la interfaz de traducción Rosetta


Rosetta es una aplicación de terceros que te permite editar traducciones utilizando la misma interfaz que el sitio de administración de Django. Rosetta facilita la edición de archivos .po y actualiza los archivos de traducción compilados. Vamos a agregarlo al proyecto.


Instala Rosetta a través de pip usando este comando:

```
pip install django-rosetta
```

Luego, agrega 'rosetta' a la configuración INSTALLED_APPS en el archivo settings.py de tu proyecto, de la siguiente manera:

```
INSTALLED_APPS = [
    # ...
    'rosetta',
]
```
Necesitas agregar las URL de Rosetta a tu configuración principal de URL. Edita el archivo urls.py principal de tu proyecto y agrega el siguiente patrón de URL resaltado en azul:

PracticaDjango/PracticaDjango/urls.py

#...
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('Proyecto_web_app.urls')),
    path('servicios/', include('Servicios.urls')),
    path('blog/', include('Blog.urls')),
    path('contacto/', include('Contacto.urls')),
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
    path('rosetta/', include('rosetta.urls')),
    path('payment/', include('Payment.urls', namespace='payment')),
    path('cupones/', include('Cupones.urls', namespace='cupones')),
    path('tienda/', include('Tienda.urls')),
    path('carro/', include('Carro.urls')),
    path('cuenta/', include('Autentificacion.urls')),
    path('social-auth/', include('social_django.urls', namespace='social')),
    path('orders/', include('Orders.urls', namespace='orders')),
    path('__debug__/', include('debug_toolbar.urls')),    
]

Asegúrate de colocarlo antes del patrón de URL de las aplicaciones de la Tienda para evitar una coincidencia no deseada de patrones.

Abre http://127.0.0.1:8000/admin/ e inicia sesión con un superusuario. Luego, navega a http://127.0.0.1:8000/rosetta/ en tu navegador. En el menú Filtro, haz clic en TERCEROS para mostrar todos los archivos de mensajes disponibles.

Deberías ver una lista de idiomas existentes, como esta:

imagen de rosetta


Haz clic en la aplicación Practicadjango de la sección Inglés para editar las traducciones al Inglés. Deberías ver una lista de las cadenas de traducción, algo parecido a esto:


cadenas traducidas



Como ves, puedes introducir las traducciones en la columna dedicada al idioma a traducir, el inglés en este caso. Si hubiésemos utilizado marcadores de posición estas aparecerían de la siguiente forma:

  %(items)s producto, $%(total)s          %(items)s item, $%(total)s 
  %(items)s productos, $%(total)s          %(items)s items, $%(total)s      
Roseta utiliza un color de fondo diferente para mostrar los marcadores de posición. Cuando los traduzcas, asegúrate de mantener los marcadores de posición inalterados.

Cuando termines de editar las traducciones, haz clic en el botón Guardar y traducir el siguiente bloque para guardar las traducciones en el archivo .po. Rosetta compila el archivo de mensajes cuando guardas las traducciones, por lo que no es necesario que ejecutes el comando compilemessages. Sin embargo, Rosetta requiere acceso de escritura a los directorios de localización para escribir los archivos de mensajes. Asegúrate de que los directorios tengan permisos válidos.

Si deseas que otros usuarios puedan editar las traducciones, abre http://127.0.0.1:8000/admin/auth/group/add/ en tu navegador y crea un nuevo grupo llamado traductores. Luego, accede a http://127.0.0.1:8000/admin/auth/user/ para editar los usuarios a quienes deseas otorgar permisos para que puedan editar las traducciones. Al editar un usuario, en la sección Permisos, añade el grupo de traductores a los Grupos elegidos para cada usuario. Rosetta solo está disponible para superusuarios o usuarios que pertenezcan al grupo de traductores.

Puedes leer la documentación de Rosetta en https://django-rosetta.readthedocs.io/.

Cuando agregues nuevas traducciones a tu entorno de producción, si usas Django con un servidor web real, deberás recargar tu servidor después de ejecutar el comando compilemessages o después de guardar las traducciones con Rosetta para que los cambios surtan efecto.


Fuzzy Translations.


Al editar traducciones en Rosetta, puedes ver una columna llamada FUZZY. Esta no es una característica de Rosetta; es proporcionada por gettext. Si la bandera FUZZY está activa para una traducción, no se incluirá en los archivos de mensajes compilados. Esta bandera marca las cadenas de traducción que necesitan ser revisadas por un traductor. Cuando los archivos .po se actualizan con nuevas cadenas de traducción, es posible que algunas cadenas de traducción se marquen automáticamente como difusas. Esto ocurre cuando gettext encuentra algún msgid que ha sido ligeramente modificado. gettext lo empareja con lo que cree que era la antigua traducción y lo marca como difuso para su revisión. El traductor debe revisar las traducciones difusas, eliminar la bandera FUZZY y compilar nuevamente el archivo de traducción.


Patrones de URL para internacionalización


Django ofrece capacidades de internacionalización para las URL. Incluye dos características principales para las URL internacionalizadas:

• Prefijo de idioma en patrones de URL: Agregar un prefijo de idioma a las URL para servir cada versión de idioma bajo una URL base diferente.
• Patrones de URL traducidos: Traducir los patrones de URL para que cada URL sea diferente para cada idioma.

Una razón para traducir las URL es optimizar tu sitio para los motores de búsqueda. Al agregar un prefijo de idioma a tus patrones, podrás indexar una URL para cada idioma en lugar de una única URL para todos ellos. Además, al traducir las URL a cada idioma, proporcionarás a los motores de búsqueda URLs que se clasificarán mejor para cada idioma.


Agregar un prefijo de idioma a los patrones de URL


Django te permite agregar un prefijo de idioma a tus patrones de URL. Por ejemplo, la versión en inglés de tu sitio puede ser servida bajo una ruta que comienza con /en/, y la versión en español bajo /es/. Para usar idiomas en los patrones de URL, debes utilizar el LocaleMiddleware proporcionado por Django. El framework lo utilizará para identificar el idioma actual a partir de la URL solicitada. Anteriormente, lo agregaste a la configuración MIDDLEWARE de tu proyecto, así que no necesitas hacerlo ahora.

Agreguemos un prefijo de idioma a tus patrones de URL. Edita el archivo urls.py principal del proyecto  y agrega i18n_patterns(), de la siguiente manera:

PracticaDjango/PracticaDjango/urls.py

from django.conf.urls.i18n import i18n_patterns
#...
urlpatterns = i18n_patterns(
    path('admin/', admin.site.urls),
    path('', include('Proyecto_web_app.urls')),
    path('servicios/', include('Servicios.urls')),
    path('blog/', include('Blog.urls')),
    path('contacto/', include('Contacto.urls')),
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
    path('rosetta/', include('rosetta.urls')),
    path('payment/', include('Payment.urls', namespace='payment')),
    path('cupones/', include('Cupones.urls', namespace='cupones')),
    path('tienda/', include('Tienda.urls')),
    path('carro/', include('Carro.urls')),
    path('cuenta/', include('Autentificacion.urls')),
    path('social-auth/', include('social_django.urls', namespace='social')),
    path('orders/', include('Orders.urls', namespace='orders')),
    path('__debug__/', include('debug_toolbar.urls')),    
)

Puedes combinar patrones de URL estándar no traducibles y patrones bajo i18n_patterns para que algunos patrones incluyan un prefijo de idioma y otros no lo hagan. Sin embargo, es mejor usar URLs traducidas únicamente para evitar la posibilidad de que una URL traducida descuidadamente coincida con un patrón de URL no traducido.

Ejecuta el servidor de desarrollo y abre http://127.0.0.1:8000/ en tu navegador. Django realizará los pasos descritos en la sección "Cómo Django determina el idioma actual" para determinar el idioma actual, y te redirigirá a la URL solicitada, incluyendo el prefijo de idioma. Echa un vistazo a la URL en tu navegador; ahora debería lucir como http://127.0.0.1:8000/es/. El idioma actual es el establecido por el encabezado Accept-Language de tu navegador si es español o inglés; de lo contrario, es el LANGUAGE_CODE predeterminado (español) definido en tu configuración.


Traducir patrones de URL


Django soporta cadenas traducidas en patrones de URL. Puedes usar una traducción diferente para cada idioma para un solo patrón de URL. Puedes marcar los patrones de URL para traducción de la misma manera que lo harías con literales, utilizando la función gettext_lazy().

Edita el archivo urls.py principal del proyecto y añade cadenas de traducción a las expresiones regulares de los patrones de URL para las aplicaciones de carrito, pedidos, pagos y cupones, de la siguiente manera:

PracticaDjango/PracticaDjango/urls.py

from django.utils.translation import gettext_lazy as _

#...

urlpatterns = i18n_patterns(
    path('admin/', admin.site.urls),
    path('', include('Proyecto_web_app.urls')),
    path(_('servicios/'), include('Servicios.urls')),
    path('blog/', include('Blog.urls')),
    path(_('contacto/'), include('Contacto.urls')),
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
    path('rosetta/', include('rosetta.urls')),
    path('payment/', include('Payment.urls', namespace='payment')),
    path(_('cupones/'), include('Cupones.urls', namespace='cupones')),
    path(_('tienda/'), include('Tienda.urls')),
    path(_('carro/'), include('Carro.urls')),
    path(_('cuenta/'), include('Autentificacion.urls')),
    path('social-auth/', include('social_django.urls', namespace='social')),
    path('orders/', include('Orders.urls', namespace='orders')),
    path('__debug__/', include('debug_toolbar.urls')),    
)
Al igual que estos patrones puedes traducir cualquier otro de otro archivo urls.py. Vamos a detenernos un momento en la aplicación Payment porque, aunque cuando se creo las variables están en inglés y por tanto no haría falta traducirlas, creo que es interesante verlo por el tema de la pasarela de pago que usa webhooks.

Edita el código del archivo urls.py de la aplicación Payment de esta forma:

PracticaDjango/Payment/urls.py

from django.urls import path
from . import views
from . import webhooks

# Para la internacionalización
from django.utils.translation import gettext_lazy as _

app_name = 'payment'

urlpatterns = [
    path(_('process/'), views.payment_process, name='process'),
    path(_('completed/'), views.payment_completed, name='completed'),
    path(_('canceled/'), views.payment_canceled, name='canceled'),
    path('webhook/', webhooks.stripe_webhook, name='stripe-webhook'),
]
Ten en cuenta que estos patrones de URL incluirán un prefijo de idioma porque están incluidos bajo i18n_patterns() en el archivo urls.py principal del proyecto. Esto hará que cada patrón de URL tenga un URI diferente para cada idioma disponible, uno que comienza con /es/, otro con /en/, y así sucesivamente. Sin embargo, necesitamos una sola URL para que Stripe notifique eventos, y necesitamos evitar los prefijos de idioma en la URL del webhook.

Elimina el patrón de URL del webhook del archivo urls.py de la aplicación de pagos. El archivo debería verse así ahora:

PracticaDjango/Payment/urls.py

from django.urls import path
from . import views
from . import webhooks

# Para la internacionalización
from django.utils.translation import gettext_lazy as _

app_name = 'payment'

urlpatterns = [
    path(_('process/'), views.payment_process, name='process'),
    path(_('completed/'), views.payment_completed, name='completed'),
    path(_('canceled/'), views.payment_canceled, name='canceled'),
]
Luego, agrega el siguiente patrón de URL del webhook al archivo urls.py principal del proyecto PracticaDjango. El nuevo código está resaltado en negrita:

PracticaDjango/Payment/urls.py

#...
# Para la internacionalización del proyecto
from django.conf.urls.i18n import i18n_patterns
from django.utils.translation import gettext_lazy as _
from Payment import webhooks

#...
urlpatterns = i18n_patterns(
    path('admin/', admin.site.urls),
    path('', include('Proyecto_web_app.urls')),
    path(_('servicios/'), include('Servicios.urls')),
    path('blog/', include('Blog.urls')),
    path(_('contacto/'), include('Contacto.urls')),
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
    path('rosetta/', include('rosetta.urls')),
    path('payment/', include('Payment.urls', namespace='payment')),
    path(_('cupones/'), include('Cupones.urls', namespace='cupones')),
    path(_('tienda/'), include('Tienda.urls')),
    path(_('carro/'), include('Carro.urls')),
    path(_('cuenta/'), include('Autentificacion.urls')),
    path('social-auth/', include('social_django.urls', namespace='social')),
    path('orders/', include('Orders.urls', namespace='orders')),
    path('__debug__/', include('debug_toolbar.urls')),
)

urlpatterns += [path('payment/webhook/', webhooks.stripe_webhook, name='stripe-webhook'),
]

Hemos agregado el patrón de URL del webhook a urlpatterns fuera de i18n_patterns() para asegurarnos de mantener una única URL para las notificaciones de eventos de Stripe.

Abre la terminal y ejecuta el siguiente comando para actualizar los archivos de mensajes con las nuevas traducciones:

```
django-admin makemessages --all
```

Asegúrate de que el servidor de desarrollo esté en funcionamiento con el siguiente comando:

```
python manage.py runserver
```

Abre http://127.0.0.1:8000/es/rosetta/ en tu navegador y haz clic en el enlace de PracticaDjango bajo la sección de inglés. Haz clic en SIN TRADUCIR SOLO para ver solo las cadenas que aún no se han traducido. Ahora verás los patrones de URL para traducción, como se muestra en la Figura 11.4:


cadenas sin traducir


Añade las traducciones para cada URL. No olvides incluir la barra invertida / al final de cada URL como se muestra en la siguiente figura:


traducción de cadenas


Cuando hayas terminado haz clic en GUARDAR Y TRADUCIR SIGUIENTE BLOQUE

En el bloque A REVISAR SOLO aparecerán aquellas traducciones que han sido marcadas porque exista alguna duda sobre su corrección.


Permitir a los usuarios cambiar de idioma


Dado que estás sirviendo contenido que está disponible en varios idiomas, deberías permitir que tus usuarios cambien el idioma del sitio. Vamos a añadir un selector de idioma a nuestro sitio. El selector de idioma consistirá en una lista de idiomas disponibles mostrados utilizando enlaces.

Edita la plantilla base.html de la aplicación de la aplicacion Proyecto_web_app y añade el siguiente código:

PracticaDjango/Proyecto_web_app/templates/Proyecto_web_app/base.html

{# para internacionalizar del proyecto#}
{% load i18n %}

<header>
        <h1 class="container-fluid text-center text-white bg-dark py-5">UnikGAME TIENDA VIRTUAL</h1>
        <div style="display: flex; justify-content: space-between; align-items: center;">

            {% get_current_language as LANGUAGE_CODE %}
            {% get_available_languages as LANGUAGES %}
            {% get_language_info_list for LANGUAGES as languages %}
            <div class="languages">
                <p>{% trans "Idioma" %}:</p>
                <ul class="languages">
                    {% for language in languages %}
                    <li>
                        <a href="/{{ language.code }}/"{% if language.code == LANGUAGE_CODE %} class="selected"{% endif %}>
                            {{ language.name_local }}
                        </a>
                    </li>                    
                    {% endfor %}
                </ul>
            </div>
        </div>
        <div style="color: black; text-align: right; margin-right: 100px;">
            {% if user.is_authenticated %}
            Hola, {{user.username}}&nbsp;&nbsp;|&nbsp;&nbsp;

Para que quede algo más bonito añade lo siguiente al archivo style.css de la aplicación:

PracticaDjango/Proyecto_web_app/static/Proyecto_web_app/css/style.css

.languages ul {
     list-style:none;
     margin:0;
     padding:0;
     float:left;
 }

 .languages ul li {
    float:left;
    margin:0 0 0 10px;
    font-size:14px;
}

.languages p {
    float:left;
    margin:0;
}

.selected {
    color: black;
    background:#e1ebf1;
    border-radius:4px;
}

Asegúrate de que ninguna etiqueta de plantilla esté dividida en múltiples líneas. Así es como construyes tu selector de idioma:

1. Cargas las etiquetas de internacionalización usando {% load i18n %}.
2. Utilizas la etiqueta {% get_current_language %} para recuperar el idioma actual.
3. Obtienes los idiomas definidos en la configuración LANGUAGES utilizando la etiqueta de plantilla {% get_available_languages %}.
4. Utilizas la etiqueta {% get_language_info_list %} para proporcionar un acceso fácil a los atributos del idioma.
5. Construyes una lista HTML para mostrar todos los idiomas disponibles y añades un atributo de clase seleccionada al idioma activo actual.

En el código para el selector de idioma, utilizaste las etiquetas de plantilla proporcionadas por i18n, basadas en los idiomas disponibles en la configuración de tu proyecto. Ahora abre http://127.0.0.1:8000/ en tu navegador y echa un vistazo. Deberías ver el selector de idioma en la esquina superior derecha del sitio, como sigue:


Selección de idioma



Traduciendo los campos del modelo. 


Si necesitas traducir los objetos que se crean con el modelo puedes buscar información sobre los paquetes django-vinaigrette, django-parler o django-modeltranslation.


Localización de formato

Dependiendo de la configuración regional del usuario, es posible que desees mostrar fechas, horas y números en diferentes formatos. Esto puede activarse cambiando la configuración USE_L10N a True en el archivo settings.py de tu proyecto.

Cuando USE_L10N está habilitado, Django intentará usar un formato específico para la configuración regional cada vez que emita un valor en una plantilla. Puedes ver que los números decimales en la versión en inglés de tu sitio se muestran con un punto como separador de decimales, mientras que en la versión en español, se muestran utilizando una coma. Esto se debe a los formatos regionales especificados para la configuración regional 'es' por Django. Puedes echar un vistazo a la configuración de formato español en https://github.com/django/django/blob/stable/5.0.x/django/conf/locale/es/formats.py.

Normalmente, establecerás la configuración USE_L10N en True y dejarás que Django aplique la localización de formato para cada configuración regional. Sin embargo, puede haber situaciones en las que no quieras usar valores localizados. Esto es especialmente relevante al emitir JavaScript o JSON, que debe proporcionar un formato legible por máquina.

Django ofrece una etiqueta de plantilla {% localize %} que te permite activar/desactivar la localización para fragmentos de plantilla. Esto te da control sobre la formateación localizada. Deberás cargar las etiquetas l10n para poder usar esta etiqueta de plantilla. El siguiente es un ejemplo de cómo activar y desactivar la localización en una plantilla:

```

{% load l10n %}

{% localize on %}

{{ valor }}

{% endlocalize %}

{% localize off %}

{{ valor }}

{% endlocalize %}

```

Django también ofrece los filtros de plantilla localize y unlocalize para forzar o evitar la localización de un valor. Estos filtros se pueden aplicar de la siguiente manera:

```django

{{ valor|localize }}

{{ valor|unlocalize }}

```

También puedes crear archivos de formato personalizados para especificar el formato regional. Puedes encontrar más información sobre la localización de formato en https://docs.djangoproject.com/en/4.1/topics/i18n/formatting/.


Utilizando django-localflavor para validar campos de formulario


django-localflavor es un módulo de terceros que contiene una colección de utilidades, como campos de formulario o campos de modelo, que son específicos para cada país. Es muy útil para validar regiones locales, números de teléfono locales, números de documento de identidad, números de seguridad social, etc. El paquete está organizado en una serie de módulos nombrados según los códigos de país ISO 3166.

Instala django-localflavor usando el siguiente comando:

pip install django-localflavor

Edita el archivo settings.py de tu proyecto y añade localflavor al ajuste INSTALLED_APPS, de la siguiente manera:

```

INSTALLED_APPS = [

    # ...

    'localflavor',

]

```

Vas a añadir el campo de código postal de España para que se requiera un código postal válido de al crear un nuevo pedido.

Edita el archivo forms.py de la aplicación de pedidos y haz que se vea como sigue:

from django import forms
from localflavor.es.forms import ESPostalCodeField
from .models import Order
class OrderCreateForm(forms.ModelForm):
    codigo_postal = ESPostalCodeField()

    #...

Importas el campo ESPostalCodeField del paquete es de localflavor y lo utilizas para el campo código postal del formulario OrderCreateForm.

Ejecuta el servidor de desarrollo con el siguiente comando:

```

python manage.py runserver

```

Abre http://127.0.0.1:8000/en/orders/create/ en tu navegador. Rellena todos los campos, introduce un código postal de site números y luego envía el formulario. Obtendrás el siguiente error de validación, que es generado por ESPostalCodeField:

```

Introduce un código postal en el formato XXXXX

```

Esto es solo un breve ejemplo de cómo utilizar un campo personalizado de localflavor en tu propio proyecto con fines de validación. Los componentes locales proporcionados por localflavor son muy útiles para adaptar tu aplicación a países específicos. Puedes leer la documentación de django-localflavor y ver todos los componentes locales disponibles para cada país en https://django-localflavor.readthedocs.io/en/latest/.


miércoles, 7 de febrero de 2024

26.- Construyendo un motor de recomendaciones de productos. (Redis)

Un motor de recomendación es un sistema que predice la preferencia o calificación que un usuario daría a un artículo. El sistema selecciona artículos relevantes para un usuario basándose en su comportamiento y el conocimiento que tiene sobre ellos. Hoy en día, los sistemas de recomendación se utilizan en muchos servicios en línea. Ayudan a los usuarios al seleccionar cosas en las que podrían estar interesados entre la gran cantidad de datos disponibles que les resultan irrelevantes. Ofrecer buenas recomendaciones mejora la participación del usuario. Los sitios de comercio electrónico también se benefician al ofrecer recomendaciones de productos relevantes al aumentar su ingreso promedio por usuario.

Vas a crear un motor de recomendación simple pero potente que sugiera productos que generalmente se compran juntos. Vamos a sugerir productos basados en ventas históricas, identificando así los productos que normalmente se compran conjuntamente. También vamos a sugerir productos complementarios en dos escenarios diferentes:

- Página de detalle del producto: Mostraremos una lista de productos que generalmente se compran con el producto dado. Esto se mostrará como "Los usuarios que compraron esto también compraron X, Y, y Z". Necesitas una estructura de datos que te permita almacenar el número de veces que cada producto se ha comprado junto con el producto que se muestra.

- Página de detalle del carrito: Basándote en los productos que los usuarios agregan al carrito, vas a sugerir productos que generalmente se compran junto con estos. En este caso, la puntuación que calculas para obtener productos relacionados tiene que ser añadido.

Vas a utilizar Redis para almacenar los productos que generalmente se compran juntos. 

Redis es una base de datos avanzada de clave/valor que te permite guardar diferentes tipos de datos. También cuenta con operaciones de E/S extremadamente rápidas. Redis almacena todo en memoria, pero los datos pueden guardarse al volcar el conjunto de datos al disco de vez en cuando, o al agregar cada comando a un registro. Redis es muy versátil en comparación con otras bases de datos clave/valor: proporciona un conjunto de comandos poderosos y admite diversas estructuras de datos, como cadenas, hashes, listas, conjuntos, conjuntos ordenados e incluso mapas de bits o Hyper-LogLogs.

Aunque SQL es más adecuado para el almacenamiento de datos persistentes, Redis ofrece numerosas ventajas al tratar con datos que cambian rápidamente, almacenamiento volátil o cuando se necesita un caché rápido. Veamos cómo Redis puede ser utilizado para agregar nuevas funcionalidades al proyecto.

Puedes encontrar más información sobre Redis en su página de inicio en https://redis.io/.

Redis proporciona una imagen de Docker que facilita mucho desplegar un servidor Redis con una configuración estándar.


Instalando Redis con Docker.

Después de instalar Docker en tu máquina Linux, macOS o Windows, puedes descargar fácilmente la imagen de Docker de Redis. Ejecuta el siguiente comando desde la terminal:


$ sudo docker pull redis


Esto descargará la imagen de Docker de Redis en tu máquina local. Puedes encontrar información sobre la imagen de Docker oficial de Redis en https://hub.docker.com/_/redis. También puedes encontrar otros métodos alternativos para instalar Redis en https://redis.io/download/.

Ejecuta el siguiente comando en la terminal para iniciar el contenedor de Docker de Redis:


sudo docker run -it --rm --name redis -p 6379:6379 redis


Con este comando, ejecutamos Redis en un contenedor de Docker. La opción -it le indica a Docker que te lleve directamente dentro del contenedor para una entrada interactiva. La opción --rm le dice a Docker que limpie automáticamente el contenedor y elimine el sistema de archivos cuando el contenedor se cierre. La opción --name se utiliza para asignar un nombre al contenedor. La opción -p se utiliza para publicar el puerto 6379, en el que se ejecuta Redis, en el mismo puerto de la interfaz del host. 6379 es el puerto predeterminado para Redis.

Deberías ver una salida que termine con las siguientes líneas:

# Server initialized
* Ready to accept connections tcp


Mantén el servidor Redis en funcionamiento en el puerto 6379 y abre otra terminal. Inicia el cliente Redis con el siguiente comando:

$ sudo docker exec -it redis sh


Verás una línea con el símbolo de almohadilla (#):

#

Inicia el cliente Redis con el siguiente comando:

# redis-cli

Verás el indicador del shell del cliente Redis, como esto:

127.0.0.1:6379>

El cliente Redis te permite ejecutar comandos de Redis directamente desde el shell. Vamos a probar algunos comandos.

Ingresa el comando SET en el shell de Redis para almacenar un valor en una clave:

127.0.0.1:6379> SET name "Yasmina"
OK

El comando anterior crea una clave "name" con el valor de cadena "Yasmina" en la base de datos de Redis. La salida OK indica que la clave se ha guardado correctamente.

A continuación, recupera el valor usando el comando GET, de la siguiente manera:

127.0.0.1:6379> GET name
"Yasmina"

También puedes verificar si una clave existe usando el comando EXISTS. Este comando devuelve 1 si la clave dada existe, y 0 en caso contrario:

127.0.0.1:6379> EXISTS name
(integer) 1

Puedes establecer el tiempo de expiración de una clave usando el comando EXPIRE, que te permite establecer el tiempo de vida en segundos. Otra opción es usar el comando EXPIREAT, que espera una marca de tiempo Unix. La expiración de la clave es útil para usar Redis como caché o para almacenar datos volátiles:

127.0.0.1:6379> GET name
"Yasmina"
127.0.0.1:6379> EXPIRE name 2
(integer) 1

Espera más de dos segundos e intenta obtener la misma clave nuevamente:

127.0.0.1:6379> GET name
(nil)

La respuesta (nil) es una respuesta nula y significa que no se ha encontrado ninguna clave. También puedes eliminar cualquier clave usando el comando DEL, de la siguiente manera:

127.0.0.1:6379> SET total 1
OK
127.0.0.1:6379> DEL total
(integer) 1
127.0.0.1:6379> GET total
(nil)

Estos son solo comandos básicos para operaciones de claves. Puedes encontrar todos los comandos de Redis en https://redis.io/commands/ y todos los tipos de datos de Redis en https://redis.io/docs/manual/data-types/.

Usar Redis con Python

Necesitarás los enlaces de Python para Redis. Instala redis-py a través de pip usando el siguiente comando:

$ pip install redis

Puedes encontrar la documentación de redis-py en https://redis-py.readthedocs.io/.

El paquete redis-py interactúa con Redis, proporcionando una interfaz de Python que sigue la sintaxis de los comandos de Redis. Abre el shell de Python con el siguiente comando:

$ python manage.py shell

Ejecuta el siguiente código:

>>> import redis

>>> r = redis.Redis(host='localhost', port=6379, db=0)

El código anterior crea una conexión con la base de datos de Redis. En Redis, las bases de datos se identifican por un índice entero en lugar de un nombre de base de datos. Por defecto, un cliente está conectado a la base de datos 0. El número de bases de datos de Redis disponibles se establece en 16, pero puedes cambiar esto en el archivo de configuración redis.conf.

A continuación, establece una clave usando el shell de Python:

>>> r.set('foo', 'bar')

True

El comando devuelve True, lo que indica que la clave se ha creado correctamente. Ahora puedes recuperar la clave usando el comando get():

>>> r.get('foo')

b'bar'

Como podrás observar en el código anterior, los métodos de Redis siguen la sintaxis de los comandos de Redis.

Integremos Redis en nuestro proyecto. Edita el archivo settings.py del proyecto y añade la siguiente configuración:

PracticaDjango/PracticaDjango/settings.py

# Configuración para Redis
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 1

Estas son la configuración requerida para establecer una conexión con el servidor Redis. Crea un nuevo archivo dentro del directorio de aplicaciones de la tienda y llámalo recomendar.py. Agrega el siguiente código:

PracticaDjango/Tienda/recomendar.py

import redis
from django.conf import settings
from .models import Producto

# Conectamos a redis
r = redis.Redis(host=settings.REDIS_HOST,
                port=settings.REDIS_PORT,
                db=settings.REDIS_DB)

class Recomendar():
    def obtener_key_producto(self, id):
        return f'producto:{id}:comprado_junto'
    
    def productos_comprados(self, productos):
        productos_ids = [p.id for p in productos]
        for producto_id in productos_ids:
            for junto_id in productos_ids:
                # obtenemos el otro producto comprado junto con este
                if producto_id != junto_id:
                    # incrementamos el contador de junto comprado
                    r.zincrby(self.obtener_key_producto(producto_id),
                              1, junto_id)


Esta es la clase Recomendar, que te permitirá almacenar compras de productos y recuperar sugerencias de productos para un producto o productos dados.

El método obtener_key_producto() recibe un ID de un objeto Producto y construye la clave Redis para el conjunto ordenado donde se almacenan los productos relacionados, que se ve así: producto:[id]:comprado_junto.

El método productos_comprados() recibe una lista de objetos Producto que se han comprado juntos (es decir, pertenecen al mismo pedido).

En este método, realizas las siguientes tareas:

1. Obtienes los IDs de los productos para los objetos Producto dados.

2. Iteras sobre los IDs de los productos. Para cada ID, iteras nuevamente sobre los IDs de los productos y omites el mismo producto para que obtengas los productos que se compran junto con cada producto.

3. Obtienes la clave del producto Redis para cada producto comprado usando el método obtener_key_producto(). Para un producto con un ID de 33, este método devuelve la clave producto:33:comprado_junto. Esta es la clave para el conjunto ordenado que contiene los IDs de productos que se compraron junto con este.

4. Incrementas la puntuación de cada ID de producto contenido en el conjunto ordenado en 1. La puntuación representa el número de veces que otro producto se ha comprado junto con el producto dado.

Ahora tienes un método para almacenar y puntuar los productos que se compraron juntos. A continuación, necesitas un método para recuperar los productos que se compraron juntos para una lista de productos dados. Agrega el siguiente método sugerir_productos() a la clase Recomendar:

PracticaDjango/Tienda/recomendar.py

class Recomendar:
    #...
    def sugerir_productos(self, productos, max_resultados=6):
        productos_ids = [p.id for p in productos]
        if len(productos) == 1:
            # solamente hay un producto
            sugerencias = r.zrange(
                self.obtener_key_producto(productos_ids[0]),
                0, -1, desc=True)[:max_resultados]
        else:
            # generamos una clave temporal
            flat_ids = ''.join([str(id) for id in productos_ids])
            tmp_key = f'tmp_{flat_ids}'
            # multiples productos, combinamos las puntuaciones de los productos
            # guardamos el resultado ordenado en una clave temporal
            keys = [self.obtener_key_producto(id) for id in productos_ids]
            r.zunionstore(tmp_key, keys)
            # eliminamos los identificadores de los productos recomendados
            r.zrem(tmp_key, *productos_ids)
            # obtener los ID de los productos por su puntuación, ordenada de forma descendente
            sugerencias = r.zrange(tmp_key, 0, -1, desc=True)[:max_resultados]
            # removemos la clave temporal
            r.delete(tmp_key)
        productos_sugeridos_ids = [int(id) for id in sugerencias]
        # establecemos los productos sugeridos y los ordenamos por orden de aparición.
        productos_sugeridos = list(
            Producto.objects.filter(id__in=productos_sugeridos_ids))
        productos_sugeridos.sort(
            key=lambda x: productos_sugeridos_ids.index(x.id))
        return productos_sugeridos

El método sugerir_productos() recibe los siguientes parámetros:

- productos: Esta es una lista de objetos Producto para los que obtener recomendaciones. Puede contener uno o más productos.

- max_resultados: Este es un entero que representa el número máximo de recomendaciones a devolver.

En este método, realizas las siguientes acciones:

1. Obtienes los IDs de los productos para los objetos Producto dados.

2. Si solo se proporciona un producto, recuperas el ID de los productos que se compraron junto con el producto dado, ordenados por el número total de veces que se compraron juntos. Para hacerlo, utilizas el comando ZRANGE de Redis. Limitas el número de resultados al número especificado en el atributo max_resultados (6 por defecto).

3. Si se proporcionan más de un producto, generas una clave temporal de Redis construida con los IDs de los productos.

4. Combina y suma todos las puntuaciones para los elementos contenidos en el conjunto ordenado de cada uno de los productos dados. Esto se hace utilizando el comando ZUNIONSTORE de Redis. El comando ZUNIONSTORE realiza una unión de los conjuntos ordenados con las claves dadas y almacena la suma agregada de las puntuaciones de los elementos en una nueva clave de Redis. Puedes leer más sobre este comando en https://redis.io/commands/zunionstore/. Guardas las puntuaciones agregadas en la clave temporal.

5. Dado que estás agregando puntuaciones, es posible que obtengas los mismos productos para los que estás obteniendo recomendaciones. Los eliminas del conjunto ordenado generado utilizando el comando ZREM.

6. Recuperas los IDs de los productos de la clave temporal, ordenados por sus puntuaciones utilizando el comando ZRANGE. Limitas el número de resultados al número especificado en el atributo max_resultados. Luego, eliminas la clave temporal.

7. Finalmente, obtienes los objetos Producto con los IDs dados y ordenas los productos en el mismo orden que ellos.

Para propósitos prácticos, también añadiremos un método para borrar las recomendaciones. Agrega el siguiente método a la clase Recomendar:

PracticaDjango/Tienda/recomendar.py

class Recomendar:
    #...
    def limpiar_compras(self):
        for id in Producto.objects.values_list('id', flat=True):
            r.delete(self.obtener_key_producto(id))

Una vez que tenemos el código vamos a probar nuestro motor de recomendaciones. Asegurate de tener varios objetos Producto en la base de datos. En mi caso ya tengo grabados varios juegos para PS4 que son los que voy a utilizar para la prueba. Empecemos inicializando el contenedor de Redis en Docker usando el siguiente comando en una consola del terminal:

>>> sudo docker run -it --rm --name redis -p 6379:6379 redis

Abre otra consola del terminal y ejecuta el shell de Python:

>>> python manage.py shell

Asegúrate de que al menos tienes cuatro productos diferentes almacenados en tu base de datos. Vamos a empezar por recuperarlos usando sus nombres, de la siguiente forma:

>>> from Tienda.models import Producto
>>> nba2k23  = Producto.objects.get(nombre="NBA2K23")
>>> ghost  = Producto.objects.get(nombre="GHOST OF TSUSHIMA")
>>> god  = Producto.objects.get(nombre="GOD OF WAR")
>>> read = Producto.objects.get(nombre="READ DEAD REDEMPTION 2")

Luego, añadiremos algunas compras de prueba para que puedan ser procesados por el motor de recomendaciones:

>>> from Tienda.recomendar import Recomendar
>>> r = Recomendar()
>>> r.productos_comprados([nba2k23, ghost])
>>> r.productos_comprados([nba2k23, god])
>>> r.productos_comprados([ghost, nba2k23, read])
>>> r.productos_comprados([god, read])
>>> r.productos_comprados([nba2k23, read])
>>> r.productos_comprados([ghost, god])

Con lo cual hemos guardado las siguientes puntuaciones para los productos:

nba2k23:    ghost(2), read(2), god(1)
ghost:      nba2k23(2), read(1), god(1)
god:        nba2k23(1), read(1), ghost(1)
read:       nba2k23(2), ghost(1), god(1)

Esta es una representación de los productos que se han comprado junto con cada uno de los productos, incluyendo cuantas veces se han comprado juntos.

Recuperemos las recomendaciones para un solo producto:

>>> r.sugerir_productos([nba2k23])
[<Producto: GHOST OF TSUSHIMA>, <Producto: READ DEAD REDEMPTION 2>, <Producto: GOD OF WAR>]
>>> r.sugerir_productos([ghost])
[<Producto: NBA2K23>, <Producto: GOD OF WAR>, <Producto: READ DEAD REDEMPTION 2>]
>>> r.sugerir_productos([god])
[<Producto: GHOST OF TSUSHIMA>, <Producto: READ DEAD REDEMPTION 2>, <Producto: NBA2K23>]
>>> r.sugerir_productos([read])
[<Producto: NBA2K23>, <Producto: GOD OF WAR>, <Producto: GHOST OF TSUSHIMA>]
>>> 

Como puedes observar el orden de recomendación de los productos está basado en su puntuación. Vamos a obtener recomendaciones para múltiples productos usando sus puntuaciones:

>>> r.sugerir_productos([nba2k23, ghost])
[<Producto: READ DEAD REDEMPTION 2>, <Producto: GOD OF WAR>]
>>> r.sugerir_productos([god, ghost])
[<Producto: NBA2K23>, <Producto: READ DEAD REDEMPTION 2>]
>>> r.sugerir_productos([read, nba2k23])
[<Producto: GHOST OF TSUSHIMA>, <Producto: GOD OF WAR>]

Como puedes ver el orden de las recomendaciones coincide con las puntuaciones. Por ejemplo, los juegos sugeridos para nba2k23 y ghost son read(2+1) y god(1+1).

Si en vez de introducir una simulación de productos comprados conjuntamente como hemos visto hasta ahora quieres basarte en los pedidos u ordenes que estén en la base de datos del proyecto puedes utilizar el siguiente código en su lugar. Empecemos inicializando el contenedor de Redis en Docker usando el siguiente comando en una consola del terminal:

>>> sudo docker run -it --rm --name redis -p 6379:6379 redis

Abre otra consola del terminal y ejecuta el shell de Python:

>>> python manage.py shell

Introduce el siguiente código:

>>> from Orders.models import OrderItem

# Obtener todos los objetos OrderItem
>>> order_items = OrderItem.objects.all()

# Crear una lista para almacenar los productos comprados, donde cada elemento representa una orden
>>> productos_comprados = []

# Inicializar una lista vacía para cada orden
>>> for _ in range(len(order_items)):
        productos_comprados.append([])

# Iterar sobre cada OrderItem y agrupar los productos por orden
>>> for order_item in order_items:
        orden_id = order_item.order_id
        producto_comprado = order_item.product
    
        # Agregar el producto a la lista correspondiente a la orden
        productos_comprados[orden_id - 1].append(producto_comprado)

# Ahora 'productos_comprados' contiene una lista de listas donde cada lista interna 
# representa los productos comprados en una orden específica


Para pasar la lista de los productos comprados conjuntamente al motor de recomendación en Redis, puedes iterar sobre la lista productos_comprados y pasar cada lista de productos de cada orden al método productos_comprados de tu instancia Recomendar. Aquí te muestro cómo hacerlo:


python manage.py shell

>>> from Tienda.models import Producto
>>> from Tienda.recomendar import Recomendar

# Crear una instancia del motor de recomendación
>>> recomendador = Recomendar()

# Iterar sobre cada lista de productos comprados por orden
>>> for productos_en_orden in productos_comprados:
        # Crear una lista de instancias de Producto a partir de los nombres de los productos
        productos_instancias = [Producto.objects.get(nombre=producto.nombre) for producto in productos_en_orden]
        # Pasar la lista de instancias de Producto al método productos_comprados del motor de recomendación
        recomendador.productos_comprados(productos_instancias)

y ya solo nos queda ver si funciona el recomendador para ello instanciaremos un juego y probaremos si funciona la recomendación. Lo haremos de la siguiente forma:

>>> nba2k23  = Producto.objects.get(nombre="NBA2K23")
>>> recomendador.sugerir_productos([nba2k23])
[<Producto: READ DEAD REDEMPTION 2>, <Producto: GOD OF WAR>, <Producto: GHOST OF TSUSHIMA>]


Una vez que hemos verificado el funcionamiento correcto del motor de recomendaciones bien utilizando datos que le hemos proporcionado o usando los datos reales de los pedidos de la aplicación vamos a aplicarlo al modelo. Para ello cuando hagamos una compra y vayamos al carro la aplicación no s mostrará los productos recomendados junto con los que el usuario haya añadido al carro. Edita el archivo views.py de la aplicación Carro, importa la clase Recomendar y edita el método mostrar_carro y añade el código resaltado:

PracticaBlog/PracticaDjango/Carro/views.py

#...
# Para aplicar el recomendador de productos
from Tienda.recomendar import Recomendar

# vista para mostrar los productos del carro
def mostrar_carro(request):
    productos = Producto.objects.all()
    carro = Carro(request)
    formulario_cupon = CuponFormulario()
    # para aplicar el recomendador de productos
    r = Recomendar()
    productos_carro = [item['producto'] for item in carro]
    if(productos_carro):
        productos_recomendados = r.sugerir_productos(productos_carro, max_resultados=4)
    else:
        productos_recomendados = []
    contexto = {
        "productos": productos,
        "carro": carro,
        "formulario_cupon": formulario_cupon,
        "productos_recomendados": productos_recomendados,
    }
    return render(request, "Tienda/carro_detalle.html", contexto)
Edita la plantilla  Tienda/carro_detalle.html dentro de la aplicación Tienda y añade el siguiente código justo después de los cupones y antes del código para mostrar un texto cuando la cesta está vacia.

PracticaDjango/Tienda/templates/Tienda/carro_detalle.html

      <div>
        {% if productos_recomendados %}
        <h3 style="color:aliceblue;">Comprados juntos habitualmente</h3>
        <div class="row">
            {% for p in productos_recomendados %}
            <div class="col-md-3">
                <div class="row">
                    <div class="col-md-12">
                        <a href="{% url 'carro:agregar' p.id %}">
                            <img src="{% thumbnail p.imagen 100x0 %}" class="img-fluid">
                        </a>
                    </div>
                </div>
                <div class="row">
                    <div class="col-md-12">
                        <a href="{% url 'carro:agregar' p.id %}" style="color: black;">
                            {{ p.nombre }}
                        </a>
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
        {% endif %}
    </div>

Si ahora realizas alguna compra y entras en el carro, deberías ver una imagen muy similar a esta:

producto recomendados junto a la compra de otro

Si haces clic en alguno de ellos será añadido al carrito y el motor de recomendaciones volverá a funcionar y te recomendará nuevos productos.

Código del capítulo en GITHUB