martes, 28 de febrero de 2023

12.- Desarrollo Práctico de una aplicación. Creación Proyecto, Aplicación, Primeras vistas y Plantillas.

Vamos a crear una aplicación completa de Django y a la vez profundizando más en varios conceptos. La aplicación que crearemos tendrá cinco vistas:

  • Inicio
  • Servicios
  • Tienda 
  • Blog
  • Contacto
Todo lo que sigue ya lo hemos visto en capitulos previos, asi que no me detendré a comentarlo. Comenzaremos creando el proyecto con el nombre que queramos, en mi caso usaré PracticaDjango:


$ django-admin startproject PracticaDjango

Esto nos creará un directorio con la siguiente estructura:

PracticaDjango       > Directorio Padre 
    manage.py        > Archivo de gestión de Django.
    PracticaDjango --> Es un directorio

Es siguiente caso es crear la aplicación que gestionará la aplicación web. Así que entramos en el directorio padre y creamos la aplicación:

PracticaDjango$ python manage.py startapp Proyecto_web_app

Salida:

PracticaDjango       > Directorio Padre 
    manage.py        > Archivo de gestión de Django.
    PracticaDjango --> Es un directorio
    Proyecto_web_app > El directorio de la app recien creada.

Recuerda que una cosa es el proyecto y otra la aplicación. En Django dentro de un proyecto puedes tener muchas aplicaciones.

Ahora es un buen momento para verificar que nuestro proyecto funciona, así que vamos a la consola y ejecutamos el servidor:

PracticaDjango$ python manage.py runserver

Abrimos el navegador y entramos en localhost:8000 y si todo ha ido bien verás la siguiente pantalla:

pantalla inicio de Django

Seguidamente creamos de forma sencilla las vistas de cada una de las cinco url de la aplicación. Para ello vamos al archivo views.py y para hacer algunas pruebas importamos la clase HttpResponse.

PracticaDjango/Proyecto_web_app/views.py

from django.shortcuts import render, HttpResponse

# Create your views here.

def home(request):
    return HttpResponse('Home')

def servicios(request):
    return HttpResponse('Servicios')

def tienda(request):
    return HttpResponse('Tienda')

def blog(request):
    return HttpResponse('Blog')

def contacto(request):
    return HttpResponse('Contacto')

Ahora nos vamos a registrar las URLS. Importamos las vistas y luego registramos las direcciones.

PracticaDjango/PracticaDjango/urls.py

from django.contrib import admin
from django.urls import path
from Proyecto_web_app import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.home, name='home'),
    path('servicios/', views.servicios, name='servicios'),
    path('tienda/', views.tienda, name='tienda'),
    path('blog/', views.blog, name='blog'),
    path('contacto/', views.contacto, name='contacto'),
]

Volvemos a ejecutar el servidor, si lo hemos cerrado y probamos cada una de las url, para ver que todas las vistas funcionan.

Ej para la vista blog.

vista blog funcionando correctamente


Reorganización de las url para un mejor funcionamiento.


Hay que tener en cuenta que nosotros creamos un proyecto en Django y este puede tener varias aplicaciones. De igual forma es muy frecuente que una aplicación Django la puedas reaprovechar en diferentes proyectos. Hasta ahora registramos nuestras Urls dentro del archivo urls.py que estaba en PracticaDjango. Pero imaginaros si tuviéramos que registrar en el las urls, no de una única aplicación, sino de tres, cuatro o quince aplicaciones. En este caso este archivo tendría muchísimo código y nos seria complicado saber que urls son de una aplicación y cual de otra. Así que lo bueno es que las urls de cada aplicación estén dentro de su directorio. 

Para ello seguimos las instrucciones que ya nos indica Django en el archivo urls.py del Proyecto.

1.- Creamos un archivo llamado urls.py pero dentro de la aplicación.

2.- Luego dentro de ese archivo importamos el path

PracticaDjango/Proyecto_web_app/urls.py

from django.urls import path

3.- A continuación importamos las vistas de la aplicación.

PracticaDjango/Proyecto_web_app/urls.py

from django.urls import path
from . import views

4.- Tenemos que trabajar con la lista urlpatterns y para no tener que escribir tanto, vamos al url del proyecto, copiamos el urlpatterns y lo pegamos aquí. Lógicamente quitamos la vista admin que no corresponde a la aplicación, sino al proyecto.

PracticaDjango/Proyecto_web_app/urls.py

from django.urls import path
from Proyecto_web_app import views

app_name = "Proyecto_web_app"

urlpatterns = [
    path('', views.home, name='home'),
    path('servicios/', views.servicios, name='servicios'),
    path('tienda/', views.tienda, name='tienda'),
    path('blog/', views.blog, name='blog'),
    path('contacto/', views.contacto, name='contacto'),
]


'app_name' en Django se utiliza para definir un nombre único para la aplicación, evitando conflictos de nombres con otras aplicaciones que pudiéramos crear luego.

'name' se asigna a cada URL dentro de la aplicación y proporciona una etiqueta única para esta URL en nuestro proyecto.

Estos atributos nos ayudarán a a organizar y referenciar fácilmente las URLs en nuestro código y en las plantillas de Django.  Por ejemplo cuando más adelante tengamos que construir una URL en una plantilla en vez de usar una referencia absoluta para referirnos a la vista 'home' haremos referencia a ella como 'Proyecto_web_app:home'.

Y como hemos movido todas las vistas de la aplicación, en el archivo urls.py del PROYECTO quitamos todo lo que hemos movido y dejamos solo el admin. Quitamos también la importación de las vistas. En definitiva lo dejamos como estaba al principio. Solo nos falta enlazar el url del proyecto con el url de la aplicación, lo haremos a través del path como nos pone Django en la documentación de este mismo archivo, usando el comando 'include' que previamente habremos de importar.

PracticaDjango/PracticaDjango/ulrs.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls), 
    path('proyecto_web_app/', include('Proyecto_web_app.urls')), 
]

Llegado este punto, tenemos que comprobar que funcionan bien las urls teniendo en cuenta el path que acabamos de escribir. Para acceder a las vistas tendríamos que hacer lo siguiente. Vamos a ver como acceder a una con un ejemplo.

aceso a url tienda
Pero al hacer esto, hemos complicado un poco la url porque tenemos que agregar 'proyecto_web_app', que es el nombre de nuestro proyecto. Si queremos teclear las urls como antes, que sin poner nada nos llevaba a las vistas, tenemos que ir al urls del proyecto y en el path tenemos que dejar vacía la raíz para que no haya que poner nada y así, funcionaría como al principio.

PracticaDjango/PracticaDjango/urls.py 

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('Proyecto_web_app.urls')),  
]

url acortada de la aplicación

Plantillas de las vistas.


Nos vamos a la carpeta de la aplicación y creamos una carpeta llamada 'templates' y dentro de esta carpeta crearemos otra con el nombre también de la aplicación que será donde depositaremos las plantillas html. Tenemos que crear una plantilla para cada una de las vistas. Vamos a hacer una como ejemplo para la vista inicio pero tenemos que crear otras iguales para el resto.

PracticaDjango/Proyecto_web_app/templates/Proyecto_web_app/home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
</head>
<body>
    Home
</body>
</html>
Ahora debemos modificar el archivos de las vistas de la aplicación para que renderice las plantillas que habremos creado. Pero para que esto funcione es IMPORTANTE registrar nuestra aplicación. Tenemos que ir al archivo settings.py del proyecto y el la lista de INSTALLED_APPS tenemos que registrar la aplicación.

PracticaDjango/PracticaDjango/settings.py

...
# Application definition

INSTALLED_APPS = [
    # Nuestras aplicaciones.
    'Proyecto_web_app.apps.ProyectoWebAppConfig',
    # Aplicaciones por defecto.
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
...

Podemos aprovechar este mismo archivo para modificar nuestra hora local (TIME_ZONE) y el idioma (LANGUAGE_CODE) como ya vimos en el capitulo 1.

Después de lo anterior, ya solo nos queda modificar el archivo de las vistas de la aplicación para que se rendericen cada una de las vistas cuando se llame a la url correspondiente.

PracticaDjango/Proyecto_web_app/views.py

from django.shortcuts import render, HttpResponse

# Create your views here.

def home(request):
    return render(request, 'Proyecto_web_app/home.html')

def servicios(request):
    return render(request, 'Proyecto_web_app/servicios.html')

def tienda(request):
    return render(request, 'Proyecto_web_app/tienda.html')

def blog(request):
    return render(request, 'Proyecto_web_app/blog.html')

def contacto(request):
    return render(request, 'Proyecto_web_app/contacto.html')

domingo, 26 de febrero de 2023

11.- Django. API Forms

Django tiene principalmente dos clases para construir formularios: Form y ModelForm.

La primera es la que veremos en este capitulo. La segunda ModelForm se utiliza para crear formularios de forma dinámica a partir de los campos contenidos en modelos. La veremos en más detalle más adelante pero si quieres echarle un vistazo puedes consultar la documentación en https://docs.djangoproject.com/en/4.1/topics/forms/modelforms/.

Vamos con el uso de la clase Form.

 Documentación API Form.

Nos va a permitir crear y validar formularios de forma sencilla.

Para utilizarla lo primero que tenemos que hacer es crear en cualquier lugar de nuestro proyecto (aunque por convención se suele realizar en el mismo directorio que tengamos el views.py) un archivo llamado forms.py y dentro crearemos una clase para cada formulario que queramos crear.

gestionPedidos/forms.py

Una vez creado el archivo lo primero que tenemos que hacer es importar esta clase forms, y una vez importada crearemos una clase cuya instancia será el formulario de contacto que queremos crear. A la clase la podemos llamar como queramos y dentro de los argumentos hay poner forms.Form. A continuación especificaremos los campos que tendrá el formulario que construya esta clase.

gestionPedidos/forms.py

from django import forms

class FormularioContacto(forms.Form):
    #Especificamos los campos del formulario
    asunto = forms.CharField()
    email = forms.EmailField()
    # etiqueta textarea en django
    mensaje = forms.CharField(widget=forms.Textarea(attrs={'cols': 45, 'rows': 15}))

La clase Form es el corazón del sistema de manejo de formularios de Django. Especifica los campos en el formulario, su diseño, widgets de visualización, etiquetas, valores iniciales, valores válidos y (una vez validados) los mensajes de error asociados con campos no válidos.

Los campos que podemos utilizar en un formulario son los siguientes:

BooleanFieldCharFieldChoiceFieldTypedChoiceFieldDateField
DateTimeFieldDecimalFieldDurationFieldEmailFieldFileField
FilePathFieldFloatFieldImageFieldIntegerFieldGenericIPAddressField,
MultipleChoiceFieldTypedMultipleChoiceFieldNullBooleanFieldRegexFieldSlugField
TimeFieldURLFieldUUIDFieldComboFieldMultiValueField
SplitDateTimeFieldModelMultipleChoiceFieldModelChoiceField.

Los argumentos que son comunes a la mayoría de los campos anteriores son:

  • required: Si es True el campo no se puede dejar en blanco o dar un valor None. Los Campos son obligatorios por defecto, también puedes establecer required=False para permitir valores en blanco en el formulario.
  • label: label es usado cuando renderizamos el campo en HTML. Si label no es especificado entonces Django crearía uno a partir del nombre del campo al poner en mayúscula la primera letra y reemplazar los guiones bajos por espacios (por ejemplo. Renewal date).
  • label_suffix: Por defecto, se muestran dos puntos después de la etiqueta. Este argumento le permite especificar como sufijo diferente que contiene otros caracteres.
  • initial: El valor inicial para el campo cuando es mostrado en el formulario.
  • widget: El widget de visualización para usar.
  • help_text texto adicional que se puede mostrar en formularios para explicar cómo usar el campo.
  • error_messages: Una lista de mensajes de error para el campo. Puede reemplazarlos con sus propios mensajes si es necesario.
  • validators: Una lista de funciones que se invocarán en el campo cuando se valide.
  • localize: Permite la localización de la entrada de datos del formulario (consulte el enlace para obtener más información).
  • disabled: El campo se muestra pero su valor no se puede editar si esto es True. Por defecto es False.
Para ver como ha quedado y como crea Django el formulario vamos a abrir el shell de django para ver el resultado.

$ python manage.py shell
>>> from gestionPedidos.forms import FormularioContacto
>>> prueba = FormularioContacto()
>>> print(prueba)
La salida es el código html que se genera al instanciar la clase.

Salida:

<tr>
    <th><label for="id_asunto">Asunto</label></th>
    <td>      
      <input type="text" name="asunto" required id="id_asunto">     
    </td>
</tr>

<tr>
    <th><label for="id_email">Email:</label></th>
    <td>      
      <input type="email" name="email" required id="id_email">
     </td>
</tr>

<tr>
    <th><label for="id_mensaje">Mensaje:</label></th>
    <td>      
      <textarea name="mensaje" cols="45" rows="15" required id="id_mensaje">
      </textarea>           
    </td>
</tr>

Como se ve se lo esta formateando automáticamente con forma de tabla. Por defecto todos los campos son requeridos y también se les añade los label automáticamente.

Pero también podemos darle formato de párrafo (con etiquetas <p>).

$ python manage.py shell
>>> from gestionPedidos.forms import FormularioContacto
>>> prueba = FormularioContacto()
>>> print(prueba.as_p())
SALIDA


<p>
    <label for="id_asunto">Asunto</label>
    <input type="text" name="asunto" required id="id_asunto">
</p>

<p>
    <label for="id_email">Email:</label>
    <input type="email" name="email" required id="id_email">
</p>

<p>
    <label for="id_mensaje">Mensaje:</label>
    <textarea name="mensaje" cols="45" rows="15" required id="id_mensaje">
    </textarea>
</p>

Pero también se puede crear como una lista desordenada.

$ python manage.py shell
>>> from gestionPedidos.forms import FormularioContacto
>>> prueba = FormularioContacto()
>>> print(prueba.as_ul())
SALIDA

<li>
    
    <label for="id_asunto">Asunto</label>
    <input type="text" name="asunto" required id="id_asunto">
</li>

<li>
    <label for="id_email">Email:</label>
    <input type="email" name="email" required id="id_email">
</li>

<li>
    <label for="id_mensaje">Mensaje:</label>
    <textarea name="mensaje" cols="45" rows="15" required id="id_mensaje">
    </textarea>
</li>

Como se ve, django no incluye las etiquetas <table> ni tampoco <form>. Lo que si hace Django es validar los datos introducidos en los campos. Lo primero es que hemos visto es que los campos son requeridos, pero también le da otros tipos de validaciones. Por ejemplo en el campo email si se introduce un email no válido no lo permitiría. Esto implica que a la hora de enviar un formulario este puede ser válido o no (por ejemplo un email incorrecto o dejar en blanco un campo requerido)

Para saber si un formulario es válido tenemos el método dentro de la api forms que es is_valid(). Este método nos dice si ha pasado la validación, devolviendo True si es correcta o Falsa si no lo es. Si este método devuelve True podremos usar una propiedad que es cleaned_data. Esta propiedad devuelve los campos enviados pero ya validados. 

Vamos a verlo con un ejemplo. Creemos un formulario pasándole los campos como argumentos a través de un diccionario.

>>> mi_formulario=FormularioContacto({'asunto':'prueba','email':'usuario@correo.es','mensaje':'texto de prueba',})
>>> mi_formulario.is_valid()
True
>>> mi_formulario.cleaned_data
{'asunto': 'prueba', 'email': 'usuario@correo.es', 'mensaje': 'texto de prueba'}
>>> 


Vamos a aplicar todo esto a nuestro proyecto. Vamos al archivo de vista de contacto, donde ya teníamos un código que funcionaba correctamente, lo comentamos y vamos a crear uno nuevo con esto que hemos visto. Lo primero es importar la clase que hemos creado para poder crear el formulario.


gestionPedidos/views.py

# Para poder enviar emails a través del formulario de contacto.
from django.core.mail import send_mail
from django.conf import settings
# api de formulario
from gestionPedidos.forms import FormularioContacto

...
def contacto(request):
    # Costruyendo el mismo formulario mediante API forms
    if request.method=="POST":
        mi_formulario=FormularioContacto(request.POST)
        if mi_formulario.is_valid():
            informacion_formulario = mi_formulario.cleaned_data
            send_mail(
                informacion_formulario['asunto'],
                informacion_formulario['mensaje']+' '+ informacion_formulario['email'],
                settings.EMAIL_HOST_USER,
                [settings.EMAIL_DESTINO],
            )
            return render(request, 'gracias.html')
    else: # metodo GET
        mi_formulario=FormularioContacto()
    
    return render(request, 'formulario_contacto_api.html', {'form':mi_formulario})


La primera vez que entramos en la vista se renderiza un formulario en blanco ya que el método utilizado no es el método POST. La plantilla 'formulario_contacto_api.html' aun no esta creada pero lo haremos luego. A esa plantilla le pasamos el código html con la creación del formulario. Una vez que se rellena el formulario comprobamos que este sea válido y si lo es, pasamos la información ya filtrada a la variable informacion_formulario con la propiedad cleaned_data.  Luego accedemos a los valores de los campos a través del diccionario. 

Ya solo queda crear el archivo 'formulario_contacto_api.html'

gestionPedidos/templates/formulario_contacto_api.html

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Contacto</title>
</head>
<body>
    <h1>Contacta con nosotros.</h1>
    {% if form.errors %}
    <p style="color: red;">Por favor,revisa este campo.</p>
    {% endif %}

    <form action="" method="post">
        {% csrf_token %} 
        <table>
            {{ form.as_table}}
        </table>
        <input type="submit" value="Enviar">
    
    </form>
</body>
</html>

La API forms crea una validación por defecto, pero aun así que lo primero que haremos es prever un posible error con {% if forms.errors %} con su código para validarlo. Luego ponemos las etiquetas <form>, el token, el <table> y dentro la API form ya crea el formulario por nosotros.

El resultado es el siguiente. Si intentas por ejemplo introducir un correo electrónico no válido o dejar un campo requerido en blanco el propio formulario te avisará de ello.

Formulario Generado con el APi form

En el ejemplo anterior hemos instanciado el formulario como una tabla {{ form.as_table }}. Le hemos dicho a Django que renderice los campos del formulario usando elementos HTML para diseñar una tabla. También podríamos haberlos renderizado como un párrafo con as_p o como una lista desordenada usando as_ul. Otra opción sería renderizar cada campo iterando a través de cada uno de los campos del formulario, como en el siguiente ejemplo:


{% for field in form %}

<div>

{{ field.errors }}
{{ field.label_tag }} {{ field }}

</div>
{% endfor %}
  

Como te habrás dado cuenta también hemos añadido el {{ csrf_token }}. Esta etiqueta añade al formulario un campo oculto con un token autogenerado para evitar el ataque maliciosos cross-site request forgery (CSRF). Puedes encontrar más información sobre este ataque en https://owasp.org/www-community/attacks/csrf.

La plantilla de esta etiqueta genera un campo oculto que se renderiza de esta forma:

<input type='hidden' name='csrfmiddlewaretoken'
value='26JjKo2lcEtYkGoV9z4XmJIEHLXN5LDR' />

Por defecto Django siempre comprueba si esta esta etiqueta en todos las solicitudes ('request') que se hacen a través del método POST, por tanto recuerda incluirla en todos los formularios que se envíen a través del método POST.



sábado, 25 de febrero de 2023

Web Scraping

 ¿Que es el Web Scraping?

Es usar, en este caso, Python para extraer contenido  y datos de un sitio web. Para poder usarlo es necesario tener unos conocimientos mínimos de como se estructura el lenguaje HTML. Este se basa en etiquetas. Y precisamente esas etiquetas son las que vamos a utilizar para extraer esos datos. 

Vamos a verlo con un ejemplo.  Supongamos que nos interesa extraer el valor liquidativo de un fondo de inversión. Ese valor liquidativo se publica por ejemplo en la siguiente página web.


https://www.quefondos.com/es/fondos/ficha/index.html?isin=ES0164853014


pagina web donde se recoge el valor del fondo

Para poder extraer el valor liquidativo tenemos que instalar las librerías - BeautifulSoup - y - Request -. 

La librería BeautifulSoup nos sirve para extraer información de contenido en formato HTML o XML.

La librería Requests se utiliza en Python para hacer peticiones HTTP.

Usaremos también la librería webbrowser que ya está instalada por defecto en la biblioteca estándar de Python y que usaremos al final del programa para abrir el navegador y ver la página web de donde hemos extraído los datos. Si no tienes instaladas las librerías lo puedes hacer con:

$ pip install beautifulsoup4
$ pip install requests

 El programa empezará importando las librerías y estableciendo en una variable la página web desde donde vamos a descargar los datos.

main.py

#!/usr/bin/env python3

# Si no los tenemos, instalamos BeautifulSoup y requests
# pip install beautifulsoup4
# pip install requests

from bs4 import BeautifulSoup
import requests
import webbrowser # esta instalado por defecto, nos lleva a una pagina web desde python.

URL = "https://www.quefondos.com/es/fondos/ficha/index.html?isin=ES0164853014"

Vamos a utilizar requests para obtener un objeto donde se encuentre la página que queremos con todos sus elementos.

...
URL = "https://www.quefondos.com/es/fondos/ficha/index.html?isin=ES0164853014"
page = requests.get(URL)

Si ejecutáramos este programa y el servidor nos proporcionase la página de forma correcta, obtendríamos la siguiente respuesta.

<Response [200]>

Sin embargo lo que a nosotros nos interesa es el contenido de esa página, lo que conseguiremos usando page.content, y luego extraeremos el código HTML para poder buscar el dato que queremos con el argumento "html.parser".

...
URL = "https://www.quefondos.com/es/fondos/ficha/index.html?isin=ES0164853014"
page = requests.get(URL)
# Lo que nos interesa de la pagina es el contenido por lo que usamos page.content
# y luego que lo pase en formato html
soup = BeautifulSoup(page.content, 'html.parser')

Si imprimimos esta variable "soup" obtendremos el código HTML de la página.

Código HTML de la página. "soup" 

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

<html lang="es-ES">
<head>
<title>CAIXABANK SELECCION TENDENCIAS, FI PLUS (ES0164853014) · CAIXABANK ASSET MGMT</title>
<meta content="Fondo de inversión, Ficha, Informe, CAIXABANK SELECCION TENDENCIAS, FI PLUS, ISIN, ES0164853014, Gestora, CAIXABANK ASSET MGMT, Política de inversión, Valor liquidativo, Rentabilidad, Ranking, Riesgo, Rating VDOS, Comisiones, Gráfico, Evolución, Histórico" name="keywords"/>
<meta content="Consulta el informe de CAIXABANK SELECCION TENDENCIAS, FI PLUS (ISIN:ES0164853014). Gestionado por
CAIXABANK ASSET MGMT. Categoría VDOS: MIXTO FLEXIBLE" name="description"/>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<meta content="no-cache" http-equiv="cache-control">
<meta content="no-cache" http-equiv="pragma">
<meta content="index, follow" name="robots">
...
<div class="w100">
<h4 id="bodytitle_h3_3">Última valoración</h4>
<p>
<span class="floatleft">Valor liquidativo: </span><span class="floatright">14,677700 EUR</span>
</p>
<p>
<span class="floatleft">Patrimonio (miles de euros):</span><span class="floatright">1.338.861,29</span>
</p>
<p>
<span class="floatleft">Fecha: </span><span class="floatright">16/02/2023</span>
</p>
</div>
...


Ahora viene la parte manual del tema y lo más importante. Lo que necesitamos es encontrar dentro de que etiquetas está el valor que queremos localizar. Dentro de todo el código HTML de la página el valor liquidativo está dentro de la etiqueta <div class="w100">. Sin embargo al buscar en la página por esta etiqueta van a aparecer muchos elementos, más de los que nos gustaría, pero por lo menos ya hemos hecho una primera acotación.

Aquí os muestro el código que queremos buscar dentro de la página. Para analizar una página web una buena herramienta que se puede utilizar, es dentro del propio navegador buscar en herramientas, "herramientas para desarrolladores". Eso nos facilitará ver el código de la parte que nos interesa capturar. En nuestro caso es el siguiente:

<div class="w100">
<h4 id="bodytitle_h3_3">Última valoración</h4>
<p>
<span class="floatleft">Valor liquidativo: </span><span class="floatright">14,574500 EUR</span>
</p>
<p>
<span class="floatleft">Patrimonio (miles de euros):</span><span class="floatright">1.328.576,22</span>
</p>
<p>
<span class="floatleft">Fecha: </span><span class="floatright">19/02/2023</span>
</p>
<p>
<span class="floatleft">1 día: </span><span class="floatright"><span class="igual">0,00%</span></span>
</p>
</div>

 Para ello usaremos:

result = soup.find_all('div', class_="w100")

En la variable result se almacenará en forma de lista cada una de las etiquetas <div> que a la vez contengan la clase con el nombre "w100". Una de ellas contendrá el valor liquidativo que estamos buscando. Pero como en el código hay varias de ellas tenemos que buscar algo que diferencie la que nosotros buscamos del resto. En este caso, dentro de la etiqueta <div class="w100"> la que contiene el valor liquidativo es la que dentro tiene esta etiqueta <h4 id="body_title_h3_3>. Lo que haremos será iterar sobre la lista result buscando con i.find() aquella que contenga esta ultima etiqueta. Cuando la encontremos, usaremos el mismo método para localizar la etiqueta que contiene el valor liquidativo. El código completo de la aplicación sería el siguiente:

main.py

#!/usr/bin/env python3

# Si no los tenemos, instalamos BeautifulSoup y requests
# pip install beautifulsoup4
# pip install requests

from bs4 import BeautifulSoup
import requests
import sys
import webbrowser  # esta instalado por defecto, nos lleva a una pagina web desde python.

URL = "https://www.quefondos.com/es/fondos/ficha/index.html?isin=ES0164853014"
page = requests.get(URL)
# Lo que nos interesa de la pagina es el contenido por lo que usamos page.content
# y luego que lo pase en formato html
soup = BeautifulSoup(page.content, 'html.parser')
# Para la busqueda se pasa la etiqueta html en la que esta contenido el dato
# result = soup.find_all('span', class_="floatright")
result = soup.find_all('div', class_="w100")

valores = {}
for i in result:
    # getText() - elimina todas las etiquetas html y nos deja solo el contenido de texto.
    # find() - busca la primera conformidad con el parámetro buscado.
    # print(i.getText())
    key = i.find('h4', id="bodytitle_h3_3")
    if key is not None:
        titulo = i.find('h4', id="bodytitle_h3_3")
        valor = i.find('span', class_='floatright')
        print(titulo)
        print(valor)
        print(f"{titulo.getText()}: {valor.getText()}")
    print("------------------------------------------------------")

# Nos muestra la página web que le pasemos la url.
webbrowser.open(URL)
Finalmente, la salida que buscamos sería la siguiente:

------------------------------------------------------
<h4 id="bodytitle_h3_3">Última valoración</h4>
<span class="floatright">14,574500 EUR</span>
Última valoración: 14,574500 EUR
------------------------------------------------------
Puedes encontrar el código completo del programa en el siguiente enlace.


domingo, 12 de febrero de 2023

10.- Django. Formulario de contacto y envío de email con datos. Variables de Entorno.

En este capitulo crearemos un ejemplo de formulario de contacto, veremos el método POST y aprovecharemos para mostrar como enviar un email que nos informe que hay un nuevo usuario y nos envíe la información introducida.

Empezamos.

Para crear el formulario de contacto lo primero que debemos hacer es irnos a las vistas, al archivo views.py y al final crear una nueva vista. De momento lo único que va a hacer es devolvernos un renderizado de un archivo, que aun no hemos creado, pero que lo haremos luego (contacto.html)

gestionPedidos/views.py

...
def contacto(request):
    '''Vista para definir un formulario de contacto.'''
    return render(request, "contacto.html")

El siguiente paso es irnos a las urls para registrar la vista.

tiendaVirtual/urls.py

...
from django.contrib import admin
from django.urls import path
# Siempre hay que importar las vistas de la aplicación
from gestionPedidos import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('busqueda_juegos/', views.busqueda_juegos),
    path('buscar/', views.buscar),
    path('contacto/', views.contacto),
]
y lo que nos falta es crear en la carpeta "templates", el archivo html del formulario que será el siguiente:

gestionPedidosl/templates/contacto.html

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Contáctanos</title>
</head>
<body>
    <h1>Contacta con nosotros.</h1>
    <form action="/contacto/" method="POST">
        {% csrf_token %}
        <!-- Cuadro de texto de entrada -->
        <p>Asunto <input type="text" name="asunto"> </p>
        <p>Email <input type="text" name="email"> </p>
        <p>Mensaje<p>
        <p></p><textarea name="mensaje" rows="15" cols="45"></textarea></p>
        <input type="submit" value="Enviar">
    </form>
</body>
</html>
Es importante dentro de la etiqueta del formulario añadir {% csrf_token %} para evitar un ataque malicioso llamado "Cross Site Request Forgery".  Permite validar que las peticiones son realizadas desde un sitio web autorizado y no desde otras fuentes. Si te interesa una mayor explicación puedes encontrar la información aquí.

Otro matiz importante es que cuando pulsemos el botón enviar usaremos como forma de envío el método "POST". La diferencia con el método "GET", que usamos en el capitulo anterior, es que mientras que este envía los datos usando la URL y por tanto los podemos ver su contenido en la barra de navegación, el método POST los envía de forma que no podamos verlos (en segundo plano y ocultos para el usuario). 

Luego definimos los elementos del formulario que tendrá la siguiente forma:


formulario de contacto


De momento el formulario no hace nada, pero podemos probarlo para ver que funciona correctamente.

Ahora, para comprobar que este formulario (que está utilizando el método "POST") funciona, vamos a hacer lo siguiente. Si al dar al botón enviar todo va bien, nos devolverá un renderizado indicándonos que la información se ha enviado correctamente. Volvemos al archivo de vistas views.py:

gestionPedidos/views.py

...
def contacto(request):
    '''Vista para definir un formulario de contacto.'''
    if request.method=="POST":
        return render(request, "gracias.html")
    else:
        return render(request, "contacto.html")

La explicación es la siguiente.

La primera vez que entramos en la página del formulario, no estamos utilizando el método "POST" sino el "GET" con lo que se renderizará el formulario de contacto. Ahora bien, cuando le damos al botón enviar, entonces la información se envía de nuevo a esta vista /contacto/ usando, ahora si, el método "POST", con lo que se renderizará la página html "gracias.html", que crearemos ahora y que nos servirá para confirmar el envío y que todo funciona correctamente.

gestionPedidos/templates/gracias.html

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Enviado</title>
</head>
<body>
    <h3>Gracias por enviar la información.</h3>
</body>
</html>

Si entramos en la url /contacto/ y enviamos el formulario nos debería salir el mensaje de Enviado. (siempre que tengamos el servidor conectado, claro)


archivo gracias.html


Envío de Emails en Django.


Enviar correos con Django es muy sencillo. Para enviar correos con Django es necesario tener un servidor local de protocolo simple de transferencia de correo (SMTP), o poder acceder a un servidor SMTP externo, como tu proveedor de servicios de correo electrónico (Gmail, Yahoo, Outlook etc)

Para ello, vamos a utilizar la librería core.mail. Para poder enviar mails lo primero que tenemos es ir al archivo settings.py y configurar una serie de parámetros. En este archivo, al final del todo pondremos las siguientes instrucciones.

tiendaVirtual/settings.py

...
# Configuración de servidor de correo de django
EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = 'smtp.outlook.com' o 'smtp.gmail.com' etc
EMAIL_PORT = 587
EMAIL_HOST_USER = 'usuario@outlook.com' o 'usuario@gmail.com'
EMAIL_HOST_PASSWORD = 'la contraseña del correo'
EMAIL_USE_TLS = True
El EMAIL_HOST es el servidor de correo que vas a usar para enviar los correos. Como gmail siempre me ha dado problemas, yo personalmente utilizo outlook. Utilizaremos el método smtp para enviar los correos. El resto es buscar la configuración asociada al correo que utilices y que puedes buscar en Google. El valor por defecto sino se especifica nada el "localhost"

EMAIL_PORT es el puerto por el que se comunica el servicio SMTP, que es el protocolo que se utiliza para enviar el correo. Por defecto el puerto es el 25.
 
EMAIL_HOST_USER es tu usuario o cuenta de correo.

EMAIL_HOST_PASSWORD es la contraseña de tu correo electrónico. Si estas usando Gmail como servidor SMTP desde que implemento la verificación en dos pasos y otras medidas de seguridad, no puedes usar tu contraseña del correo directamente. En vez de ello, Google te permite crear una contraseña especifica para la aplicación desde tu cuenta.  Para ello ve al navegador y abre la siguiente dirección, https://myaccount.google.com/. En el menú de la izquierda haz click en Seguridad, verás una pantalla como esta:

The Signing in to Google page for Google accounts

En "como inicias sesión en Google", selecciona "Verificación en dos pasos". En la parte inferior de la página selecciona "Contraseñas de aplicación". Ahí introduce un nombre que te ayude a recordar dónde vas a utilizar la contraseña de la aplicación. Después selecciona "Generar". Para introducir la contraseña de aplicación, sigue las instrucciones que aparecen en pantalla. La contraseña de aplicación es el código de 16 caracteres que se genera en tu dispositivo. 

Si no puedes usar un servidor SMTP (Gmail, Outlook, yahoo o cualquier otro servicio de correo), puedes decirle a Django que envíe estos emails a la consola del sistema remplazando la entrada EMAIL_BACKEND de archivo settings.py por esta otra:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Al usar esta configuración, Django enviará los emails a la consola del sistema en lugar de enviarlos fuera de tu equipo. Este resulta bastante útil para probar la aplicación.

Para probar si la configuración funciona vamos a hacer una prueba desde consola. Nos vamos a la misma e introducimos el comando.

$ python manage.py shell

Lo primero es importar la librería core.mail y dentro de este la función sendmail(). Luego le pasamos los argumentos que nos pide. Puedes encontrar la documentación Django sobre como enviar email aqui. Quedaría algo como esto:

(miEntorno) chema@lenovo:~/Cursos/DJANGO/tiendaVirtual$ python manage.py shell
Python 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.core.mail import send_mail
>>> send_mail(
... 'Aqui el asunto del correo',
... 'Aqui el mensaje',
... 'tu_usuario@outlook.com',
... ['correo_destino@gmail.com'],
... fail_silently=False,
... )
1
Entramos en la consola interactiva de Django e importamos la librería que comentamos previamente.
Luego rellenamos los campo de asunto, mensaje etc. 'desde_correo@outlook.com' es el correo que registramos previamente en el archivo settings.py, es decir el correo que utilizamos para enviar los mensajes. 

La opción fail_silently=False final sirve para que si algo falla nos muestre las trazas del error y poder tener una idea de lo que ocurre. 
El 1 final es que el correo se envió correctamente.

Una vez que vemos que los parámetros son correctos y que nos llegan los correos electrónicos ¿Cómo hacemos para que desde nuestro formulario de contacto, lo que el usuario haya tecleado cuando le de al botón enviar nos llegue a nuestro correo electrónico?

Pues tenemos que ir al archivo de vistas y concretamente a la vista contacto. Allí tenemos que adaptar lo que hemos visto para enviar un email con los contenidos de los campos del formulario.

Lo primero empezaremos importando del módulo django.core.mail el método send_mail y también settings para poder usar las propiedades que definimos antes. Luego es hacer lo mismo que realizamos por consola. El archivo views.py quedaría tal que así.

 gestionPedidos/views.py
from django.shortcuts import render
from django.http import HttpResponse
# Para poder usar el modelo Articulos de la base de datos
from gestionPedidos.models import Articulos
# Para poder enviar emails del formulario de contacto.
from django.core.mail import send_mail
from django.conf import settings

...
def contacto(request):
    '''Vista para definir un formulario de contacto.'''
    if request.method=="POST":

        asunto = request.POST['asunto']
        mensaje = request.POST['mensaje'] + " " + request.POST['email']
        email_from = settings.EMAIL_HOST_USER

        send_mail(
            asunto,
            mensaje,
            email_from,
            ['correo_destino@correo.com'],
            )

        return render(request, "gracias.html")
    else:
        return render(request, "contacto.html")
Las variable asunto coge su valor del formulario, a través del método POST. En el formulario también llamamos asunto a la casilla de texto que recogía la información. Lo mismo ocurre con mensaje que coge su valor del mensaje que el usuario tecleo en el formulario y después de un espacio en blanco le añado el email para que cuando lo recibamos podamos contestarle si queremos.

email_from coge su valor del archivo de configuración y recordad que este correo, es la cuenta de correo que hemos configurado para enviar los archivos. Luego en una lista pondremos el correo o correo de destino a donde queremos que llegue la información de ese formulario.

Por ultimo si se ha enviado correctamente se renderizará el archivo "gracias.html".

Usar Variables de entorno para preservar datos privados.


Para preservar información importante y confidencial en nuestros proyectos de Django es útil usar las variables de entorno. De forma esquemática el proceso es el siguiente:

1.- Instalamos el siguiente paquete.

pip install django-environ

2.- Cuando este instalado, creamos un archivo llamado .env en el mismo directorio en donde este el archivo settings.py. Su contenido esta formado por parejas de clave-valor y es muy importante que no haya espacios ni antes ni después del igual ya que sino no funcionará. Por ejemplo, para usar como variables de entorno el EMAIL_HOST_USER y EMAIL_HOST_PASSWORD pondríamos lo siguiente:

tiendaVirtual/.env

EMAIL_HOST_USER=usuario@outlook.com
EMAIL_HOST_PASSWORD=#ladificilcontraseña
En el directorio base o raíz, añadimos al archivo .gitignore (o lo creamos si no lo está) lo siguiente:

*.pyc
__pycache__
db.sqlite3
/env
*.env
.vscode
Esto es para que GIT el controlador de versiones que normalmente se usa en los proyectos no haga un seguimiento de estos archivos, ni de sus valores.

Por último en el archivo settings.py en la línea de importación añadimos

tiendaVirtual/settings.py

import environ
env = environ.Env()
environ.Env.read_env()
y ya podemos usar los valores de entorno en el archivo de configuración de la siguiente forma:

tiendaVirtual/settings.py

...

# Configuración de servidor de correo de django
EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = 'smtp.outlook.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = True
Ahora bien, si en el archivo de vistas de la aplicación queremos usar para algo los valores de estas variables tendremos que importarlos desde el archivo settings.py, ya que se encuentran ahí. Lo podemos hacer usando:

from django.conf import settings
Por eso en el apartado anterior al definir las vistas usamos "email_from = settings.EMAIL_HOST_USER"