Aplicaciones de una sola página con Django
Aplicaciones de una sola página con Django
Una ventaja importante de hacer esto es que solo necesitamos modificar la parte de la
página que realmente está cambiando. Por ejemplo, si tenemos una barra de navegación
que no cambia en función de su página actual, no querríamos tener que volver a
renderizar esa barra de navegación cada vez que cambiamos a una nueva parte de la
página.
Aplicaciones de una sola página
Observa en el HTML de la derecha que tenemos tres <!DOCTYPE html>
<html lang="es">
botones y tres divs. <head>
<title>Una sola página</title>
<style>
Por el momento, los divs contienen solo una pequeña parte div {
de texto, pero podríamos imaginar que cada div contiene el }
display: none;
En muchos casos, será ineficaz cargar todo el contenido de cada página cuando visitemos un sitio por primera vez, por lo
que necesitaremos utilizar un servidor para acceder a nuevos datos.
Por ejemplo, cuando visitas un sitio de noticias, el sitio tardaría demasiado en cargar si tuviera que cargar todos los
artículos que tiene disponibles la primera vez que visita la página. Podemos evitar este problema usando una estrategia
similar a la que usamos mientras cargamos los tipos de cambio de moneda en la lección anterior.
Esta vez, analizaremos el uso de Django para enviar y recibir información desde nuestra aplicación de una sola página.
Para mostrar cómo funciona esto, echemos un vistazo a una aplicación simple de Django. Tiene dos patrones de URL en
urls.py:
urlpatterns = [
path("", views.index, name="index"),
path("secciones/<int:num>", views.seccion, name="seccion")
]
Aplicaciones de una sola página
Y dos rutas correspondientes en views.py. Observe que la ruta de la sección toma un número entero y luego devuelve
una cadena de texto basada en ese número entero como una respuesta HTTP.
from django.http import Http404, HttpResponse
from django.shortcuts import render
historial: });
Aplicaciones de una sola página
En la función mostrarSeccion, empleamos la función // Cuando se hace clic en la flecha hacia atrás, muestra la sección anterior
window.onpopstate = function(event) {
history.pushState. Esta función agrega un nuevo console.log(event.state.seccion);
elemento a nuestro historial de navegación basado en mostrarSeccion(event.state.seccion);
tres argumentos: }
function mostrarSeccion(seccion) {
Cualquier dato asociado con el estado. fetch(`/seccions/${seccion}`)
Un parámetro de título ignorado por la mayoría de .then(response => response.texto())
.then(texto => {
los navegadores web. console.log(texto);
Qué se debe mostrar en la URL document.querySelector('#contenido').innerHTML = texto;
});
El otro cambio que hacemos en el JavaScript anterior es }
la configuración del parámetro onpopstate, que
especifica lo que debemos hacer cuando el usuario hace document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('button').forEach(button => {
clic en la flecha hacia atrás. En este caso, queremos button.onclick = function() {
mostrar la sección anterior cuando se presiona el const seccion = this.dataset.seccion;
botón.
// Agregar el estado actual al historial
history.pushState({seccion: seccion}, "", `seccion${seccion}`);
Ahora, el sitio parece un poco más fácil de usar. mostrarSeccion(seccion);
};
});
});
Scroll
Para actualizar y acceder al historial del navegador, utilizamos un objeto JavaScript importante conocido como ventana.
Hay algunas otras propiedades de la ventana que podemos usar para hacer que nuestros sitios se vean mejor:
};
</script>
</head>
<body>
<p>1</p>
<p>2</p>
<!– No pongo mas para no llenar la pantalla -->
<p>99</p>
<p>100</p>
</body>
</html>
Scroll Infinito
Cambiar el color de fondo al final de la página probablemente no sea tan útil, pero es posible que queramos detectar
que estamos al final de la página si queremos implementar el desplazamiento infinito. Por ejemplo, si está en un sitio de
redes sociales, no desea tener que cargar todas las publicaciones a la vez, es posible que desee cargar las primeras diez
y luego, cuando el usuario llegue al final, cargue las diez siguientes. Echemos un vistazo a una aplicación de Django que
podría hacer esto. Esta aplicación tiene dos rutas en urls.py
urlpatterns = [
path("", views.index, name="index"),
path("posts", views.posts, name="posts")
]
Scroll Infinito
import time
def posts(request):
{
"posts": [
"Post #10",
"Post #11",
"Post #12",
"Post #13",
"Post #14",
"Post #15"
]
}
Scroll Infinito
Ahora, en el layout de index.html que carga el sitio, comenzamos con solo un div vacío en el body y algo de estilo.
Observa que cargamos nuestros archivos estáticos al principio y luego hacemos referencia a un archivo JavaScript
dentro de nuestra carpeta static. {% load static %}
<!DOCTYPE html>
<html>
<head>
<title>Mi sitio web</title>
<style>
.post {
background-color: #77dd11;
padding: 20px;
margin: 10px;
}
body {
padding-bottom: 50px;
}
</style>
<script scr="{% static 'posts/script.js' %}"></script>
</head>
<body>
<div id="posts">
</div>
</body>
</html>
Ahora, con JavaScript, esperaremos hasta que un usuario se desplace hasta el final de la página y luego cargaremos
más publicaciones usando nuestra API:
Scroll Infinito
// Comenzar con el primer post // Cargar siguiente conjunto de posts
let contador = 1; function cargar() {
// Si el scroll lega al fin, cargar los siguientes 20 // Obtener nuevos post y agregar posts
window.onscroll = () => { fetch(`/posts?start=${start}&end=${end}`)
if (window.innerHeight + window.scrollY >= .then(response => response.json())
document.body.offsetHeight) { .then(datos => {
cargar(); datos.posts.forEach(agregar_post);
} })
}; };
Podemos mitigar la cantidad de código que realmente necesitamos escribir empleando un framework de JavaScript, al
igual que empleamos Bootstrap como un framework de CSS para reducir la cantidad de CSS que realmente tenemos
que escribir.
Uno de los framework de JavaScript más populares es una librería llamada React.
Hasta ahora en este curso, hemos estado usando métodos de programación imperativos, donde le damos a la
computadora un conjunto de declaraciones para ejecutar. Por ejemplo, para actualizar el contador en una página HTML,
podríamos tener un código que se ve así:
Vista: Lógica:
<h1>0</h1> let num = parseInt(document.querySelector("h1").innerHTML);
num += 1;
document.querySelector("h1").innerHTML = num;
React
React nos permite usar programación declarativa, lo que nos permitirá simplemente escribir código explicando lo que
deseamos mostrar y no preocuparnos por cómo lo estamos mostrando. En React, un contador podría verse un poco
más parecido a esto:
Vista: Lógica:
<h1>{num}</h1> num += 1;
React
El framework de React se basa en la idea de componentes, cada uno de los cuales puede tener un estado subyacente.
Un componente sería algo que puedes ver en una página web, como una publicación o una barra de navegación, y un
estado es un conjunto de variables asociadas con ese componente.
La belleza de React es que cuando cambia el estado, React cambiará automáticamente el DOM en consecuencia. Hay
varias formas de usar React (incluido el popular comando create-react-app publicado por Facebook), pero hoy nos
centraremos en comenzar directamente en un archivo HTML. Para hacer esto, tendremos que importar tres paquetes
de JavaScript:
render() {
return (
<div>
<h1>Bienvenido</h1>
¡Hola!
</div>
);
}
}
Ahora, podemos trabajar en la función de renderizado, donde especificaremos un encabezado y un botón. También
agregaremos un detector de eventos para cuando se haga clic en el botón, lo que React hace usando el atributo
onClick:
React
render() {
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.count}>Contar</button>
</div>
);
}
Finalmente, definamos la función de conteo. Para hacer esto, usaremos la función this.setState, que puede tomar
como argumento una función del estado anterior al nuevo.
count = () => {
this.setState(state => ({
count: state.count + 1
}))
}
React
Ej.: SUMAR ELEMENTOS
Ahora que conocemos el framework de React, trabajemos en el uso de lo que hemos aprendido para crear un sitio
similar a un juego donde los usuarios resolverán problemas de suma. Comenzaremos creando un nuevo archivo con la
misma configuración que nuestras otras páginas de React. Para comenzar a crear esta aplicación, pensemos en lo que
podríamos querer realizar en el estado. Debemos incluir todo lo que pensamos que podría cambiar mientras un
usuario está en nuestra página. Establezcamos el estado para incluir:
• num1: el primer número que se agregará
• num2: el segundo número que se agregará
• respuesta: lo que el usuario ha escrito
• puntuacion: cuántas preguntas ha respondido correctamente el usuario.
React
Ej.: SUMAR ELEMENTOS
Ahora, nuestro constructor se verá así: Y usando los valores en el estado, creemos una función de render con
lo que deseamos mostrar.
constructor(props) { render() {
super(props); return (
this.state = { <div>
<div>{this.state.num1} + {this.state.num2}</div>
num1: 1, <input type="text" value={this.state.respuesta} />
num2: 1, <div> Punto: {this.state.puntaje}</div>
respuesta: '', </div>
puntaje: 0 );
}; }
}
React
Ej.: SUMAR ELEMENTOS
En este punto, el usuario no puede escribir nada en el cuadro de entrada porque su valor está fijo como this.state.response, que actualmente es
un string vacío. Para solucionar este problema, agreguemos un atributo onChange al cuadro de entrada y configurémoslo igual a una función llamada
updateRespuesta
onChange={this.updateRespuesta}
Ahora, tendremos que definir la función updateRespuesta, que toma el evento que activó la función y establece la respuesta al valor actual de la
entrada. Esta función permite al usuario escribir y almacenar lo que se haya escrito en el state.
Ahora, agreguemos la posibilidad de que un usuario envíe un problema. Primero agregaremos otro detector de eventos
y lo vincularemos a una función que escribiremos a continuación:
onKeyPress={this.inputKeyPress}
React
Ej.: SUMAR ELEMENTOS inputKeyPress = (event) => {
// Verificar si la tecla Enter fue presionada
if (event.key === 'Enter') {
Ahora, definiremos la función inputKeyPress. En esta
// Extraer respuesta
función, primero verificaremos si se presionó la tecla const respuesta = parseInt(this.state.respuesta)
Intro y luego verificaremos si la respuesta es correcta.
Cuando el usuario tiene razón, queremos aumentar la // Verificar si la respuesta es correcta
if (respuesta === this.state.num1 + this.state.num2) {
puntuación en 1, elegir números aleatorios para el this.setState(state => ({
siguiente problema y borrar la respuesta. Si la respuesta puntaje: state.puntaje + 1,
es incorrecta, queremos disminuir la puntuación en 1 y num1: Math.ceil(Math.random() * 10),
num2: Math.ceil(Math.random() * 10),
borrar la respuesta.
respuesta: ''
}));
} else {
this.setState(state => ({
puntaje: state.puntaje - 1,
respuesta: ''
}));
}
}
}
React
Ej.: SUMAR ELEMENTOS
Para dar algunos toques finales a la aplicación, agreguemos un poco de estilo a la página. Centraremos todo en la
aplicación y luego agrandaremos el problema agregando una identificación del problema al div que contiene el problema
y luego agregando el siguiente CSS a una etiqueta de estilo:
#app {
text-align: center;
font-family: sans-serif;
}
#problema {
font-size: 72px;
}
React
Ej.: SUMAR ELEMENTOS
Finalmente, agreguemos la capacidad de ganar el juego después de ganar 10 puntos. Para hacer esto, agregaremos una
condición a la función de renderizado, devolviendo algo completamente diferente una vez que tengamos 10 puntos:
render() {
// Verificar si el puntaje es 10
if (this.state.puntaje === 10) {
return (
<div id="ganador">
¡Has ganado!
</div>
);
}
return (
<div>
<div id="problema">{this.state.num1} + {this.state.num2}</div>
<input onKeyPress={this.inputKeyPress} onChange={this.updateRespuesta} type="text" value={this.state.respuesta} />
<div> Puntaje: {this.state.puntaje}</div>
</div>
);
}
React
Ej.: SUMAR ELEMENTOS
Para que la victoria sea más emocionante, también agregaremos algo de estilo al div alternativo:
#ganador {
font-size: 72px;
color: green;
}
Testing
Veamos cómo podríamos incorporar un comando para probar la función cuadrado que escribimos cuando
aprendimos Python por primera vez. Cuando la función se escribe correctamente, no sucede nada ya que
assert es True
A medida que comienzas a construir proyectos más grandes, es posible que desees considerar el uso del
desarrollo basado en pruebas, un estilo de desarrollo en el que cada vez que corrige un error, agregas una
prueba que verifica ese error a un conjunto creciente de pruebas que se ejecutan cada vez que haces
cambios.
Esto te ayudará a asegurarte de que las funciones adicionales que agregues a un proyecto no interfieran con
las funciones ya existentes. Ahora, veamos una función un poco más compleja y pensemos en cómo escribir
pruebas puede ayudarnos a encontrar errores.
Desarrollo basado en pruebas
Test-driven development
Escribiremos una función llamada es_primo que devuelve True si y solo si su entrada es un numero primo:
import math
def es_primo(n):
Podemos ver en el resultado anterior que 5 y 10 se identificaron correctamente como primos y no primos,
pero 25 se identificó incorrectamente como primos, por lo que debe haber algo mal en nuestra función.
Sin embargo, antes de ver qué está mal con nuestra función, veamos una forma de automatizar nuestras
pruebas. Una forma en que podemos hacer esto es creando un script de shell, o algún script que se
pueda ejecutar dentro de nuestra terminal. Estos archivos requieren una extensión .sh, por lo que nuestro
archivo se llamará tests0.sh.
def test_1(self):
"""Verificar que 1 no es un numero primo."""
self.assertFalse(es_primo(1))
def test_2(self):
"""Verificar que 2 no es un numero primo."""
self.assertTrue(es_primo(2))
def test_8(self):
"""Verificar que 8 no es un numero primo."""
self.assertFalse(es_primo(8))
def test_11(self):
"""Verificar que 11 no es un numero primo."""
self.assertTrue(es_primo(11))
def test_25(self):
"""Verificar que 25 no es un numero primo."""
self.assertFalse(es_primo(25))
def test_28(self):
"""Verificar que 28 no es un numero primo."""
self.assertFalse(es_primo(28))
Mientras trabajamos con esto, usaremos el proyecto de Vuelos que creamos cuando
conocimos los modelos de Django. Primero, agregaremos un método a nuestro modelo
de vuelo que verifica que un vuelo es válido al verificar dos condiciones:
class Vuelo(models.Model):
origen = models.ForeignKey(Aeropuerto, on_delete=models.CASCADE, related_name="salidas")
destino = models.ForeignKey(Aeropuerto, on_delete=models.CASCADE, related_name="arribos")
duracion = models.IntegerField()
def __str__(self):
return f"{self.id}: {self.origen} a {self.destino}"
def es_valido_vuelo(self):
return self.origen != self.destino or self.duracion > 0
Django Testing
Para asegurarnos de que nuestra aplicación funcione como se espera, cada vez que creamos una nueva
aplicación, automáticamente se nos da un archivo tests.py. Cuando abrimos este archivo por primera
vez, vemos que la biblioteca TestCase de Django se importa automáticamente:
from django.test import TestCase
Una ventaja de usar la biblioteca TestCase es que cuando ejecutamos nuestras pruebas, se creará una
base de datos completamente nueva solo con fines de prueba.
Esto es útil porque evitamos el riesgo de modificar o eliminar accidentalmente entradas existentes en
nuestra base de datos y no tenemos que preocuparnos por eliminar entradas ficticias que creamos solo
para pruebas.
Django Testing
Para comenzar a usar esta biblioteca, primero queremos importar todos nuestros modelos:
from .models import Vuelo, Aeropuerto, Pasajero
Y luego crearemos una nueva clase que amplíe la clase TestCase que acabamos de importar. Dentro de
esta clase, definiremos una función setUp que se ejecutará al inicio del proceso de prueba.
En esta función, probablemente querremos crear. Así es como se verá nuestra clase para comenzar:
class VueloTestCase(TestCase):
def setUp(self):
# Crear aeropuertos.
a1 = Aeropuerto.objects.create(codigo="AAA", ciudad="Ciudad A")
a2 = Aeropuerto.objects.create(codigo="BBB", ciudad="Ciudad B")
# Crear vuelos.
Vuelo.objects.create(origen=a1, destino=a2, duracion=100)
Vuelo.objects.create(origen=a1, destino=a1, duracion=200)
Vuelo.objects.create(origen=a1, destino=a2, duracion=-100)
Django Testing
Ahora que tenemos algunas entradas en nuestra base de datos de prueba, agreguemos algunas funciones a
esta clase para realizar algunas pruebas.
Primero, asegurémonos de que nuestros campos de salidas y arribos funcionen correctamente
intentando contar el número de salidas (que sabemos que deberían ser 3) y arribos (que deberían ser
1) desde el aeropuerto AAA:
def test_salidas_count(self):
a = Aeropuerto.objects.get(codigo="AAA")
self.assertEqual(a.salidas.count(), 3)
def test_arribos_count(self):
a = Aeropuerto.objects.get(codigo="AAA")
self.assertEqual(a.arribos.count(), 1)
Django Testing
También podemos probar la función es_valido_vuelo que agregamos a nuestro modelo
de vuelo. Comenzaremos afirmando que la función devuelve True cuando el vuelo es
válido:
def test_valido_vuelo(self):
a1 = Aeropuerto.objects.get(codigo="AAA")
a2 = Aeropuerto.objects.get(codigo="BBB")
f = Vuelo.objects.get(origen=a1, destino=a2, duracion=100)
self.assertTrue(f.es_valido_vuelo())
Django Testing
A continuación, asegurémonos de que los vuelos con destinos y duraciones no válidos
devuelvan falso:
def test_invalido_vuelo_destino(self):
a1 = Aeropuerto.objects.get(codigo="AAA")
f = Vuelo.objects.get(origen=a1, destino=a1)
self.assertFalse(f.es_valido_vuelo())
def test_invalida_vuelo_duracion(self):
a1 = Aeropuerto.objects.get(codigo="AAA")
a2 = Aeropuerto.objects.get(codigo="BBB")
f = Vuelo.objects.get(origen=a1, destino=a2, duracion=-100)
self.assertFalse(f.es_valido_vuelo())
# Nos aseguramos que los tres vuelos hayan sido retornados en el contexto
self.assertEqual(response.context["vuelos"].count(), 3)
Client Testing
De manera similar, podemos verificar para asegurarnos de obtener un código de respuesta válido para una página de
vuelo válida y un código de respuesta no válido para una página de vuelo que no existe. (Tenga en cuenta que usamos la
función Max para encontrar el id máximo, al que tenemos acceso al incluir desde django.db.models import Max
en la parte superior de nuestro archivo)
def test_pagina_vuelo_valida(self):
a1 = Aeropuerto.objects.get(codigo="AAA")
f = Vuelo.objects.get(origin=a1, destination=a1)
c = Client()
response = c.get(f"/vuelos/{f.id}")
self.assertEqual(response.status_codigo, 200)
def test_pagina_vuelo_in0valida(self):
max_id = Vuelo.objects.all().aggregate(Max("id"))["id__max"]
c = Client()
response = c.get(f"/vuelos/{max_id + 1}")
self.assertEqual(response.status_codigo, 404)
Client Testing
Finalmente, agreguemos algunas pruebas para asegurarnos de que las listas de pasajeros y no pasajeros se
generen como se esperaba:
def test_pagina_vuelo_pasajeros(self):
f = Vuelo.objects.get(pk=1)
p = Pasajero.objects.create(nombre="Juan", apellido="Perez")
f.pasajeros.add(p)
c = Client()
response = c.get(f"/vuelos/{f.id}")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["pasajeros"].count(), 1)
def test_pagina_vuelo_no_pasajeros(self):
f = Vuelo.objects.get(pk=1)
p = Pasajero.objects.create(nombre="Juan", apellido="Perez")
c = Client()
response = c.get(f"/vuelos/{f.id}")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["no_pasajeros"].count(), 1)
Selenium
Hasta ahora, hemos podido probar el código del lado del servidor que hemos escrito
usando Python y Django, pero a medida que creamos nuestras aplicaciones, querremos
tener la capacidad de crear pruebas para nuestro código también del lado del cliente.
// Inicializar variable a 0
let counter = 0;
Esto, sin embargo, se volvería muy tedioso a medida que escribe aplicaciones de una sola
página cada vez más grandes, por lo que se han creado varios frameworks que ayudan con
las pruebas en el navegador, uno de los cuales se llama Selenium.
Selenium
Con Selenium, podremos definir un archivo de prueba en Python donde podemos simular
que un usuario abre un navegador web, navega a nuestra página e interactúa con él.
Nuestra principal herramienta al hacer esto se conoce como Web Driver, que abrirá un
navegador web en tu computadora.
Veamos cómo podemos empezar a utilizar esta librería para comenzar a interactuar con
las páginas.
import os
import pathlib
import unittest
Una nota sobre las primeras líneas es que para apuntar a una página específica, necesitamos el Identificador uniforme de
recursos (URI) de esa página, que es una cadena única que representa ese recurso.
Selenium
Usando los comandos en el interprete de Python:
# Encontrar la URI de nuestro archivo recientemente creado
>>> uri = file_uri("contador.html")
def test_titulo(self):
"""Asegurarse de que el titulo es correcto"""
driver.get(file_uri("contador.html"))
self.assertEqual(driver.title, "Contador")
def test_incrementar(self):
"""Asegurarse de que la cabecera se actualizó a 1 luego de 1 click del botón incrementar"""
driver.get(file_uri("contador.html"))
incrementar = driver.find_element_by_id("incrementar")
incrementar.click()
self.assertEqual(driver.find_element_by_tag_name("h1").text, "1")
def test_decrementar(self):
"""Asegurarse que la cabecera se actualice a -1 luego de 1 click del botón incrementar"""
driver.get(file_uri("contador.html"))
decrementar = driver.find_element_by_id("decrementar")
decrementar.click()
self.assertEqual(driver.find_element_by_tag_name("h1").text, "-1")
def test_multiples_incrementar(self):
"""Asegurarse de que la cabecera se actualice a 3 luego de 3 clicks del botón incrementar"""
driver.get(file_uri("contador.html"))
incrementar = driver.find_element_by_id("incrementar")
for i in range(3):
incrementar.click()
self.assertEqual(driver.find_element_by_tag_name("h1").text, "3")
if __name__ == "__main__":
unittest.main()
CI/CD
CI / CD es un conjunto de mejores prácticas de desarrollo de software que dictan cómo
un equipo de personas escribe el código y cómo ese código se entrega posteriormente a
los usuarios de la aplicación. Como su nombre lo indica, este método consta de dos partes
principales:
• Cuando diferentes miembros del equipo están trabajando en diferentes funciones, pueden surgir muchos
problemas de compatibilidad cuando se combinan varias funcionalidades al mismo tiempo. La integración
continua permite a los equipos abordar los pequeños conflictos a medida que surgen.
• Debido a que las pruebas unitarias se ejecutan con cada combinación, cuando una prueba falla, es más fácil
aislar la parte del código que está causando el problema.
• La publicación frecuente de nuevas versiones de una aplicación permite a los desarrolladores aislar los
problemas si surgen después del lanzamiento.
• La publicación de cambios pequeños e incrementales permite a los usuarios acostumbrarse lentamente a
las nuevas funciones de la aplicación en lugar de sentirse abrumados con una versión completamente
diferente.
• No esperar a lanzar nuevas funciones permite a las empresas mantenerse a la vanguardia en un mercado
competitivo.
GRACIAS
programacionpolotic@gmail.com
PROGRAMACIÓN WEB
CON PYTHON Y JAVASCRIPT