Ya sabes crear rutas, manejar datos con JSON, hacer un CRUD completo y dominar Jinja2 con herencia de plantillas. Ahora toca darle vida y estilo a tus aplicaciones.
En este post vas a aprender a:
✅ Integrar Bootstrap en Flask para tener diseños profesionales sin escribir CSS desde cero.
✅ Manejar archivos estáticos: imágenes, videos, CSS y JS.
✅ Crear una galería de imágenes con cards y un bucle de Jinja2.
✅ Mejorar el directorio telefónico con tablas y botones estilizados.
Al terminar, tu app se verá así: profesional, responsive y con una galería dinámica.
🤔 ¿Por qué necesitamos Bootstrap si HTML ya funciona?
Es una pregunta que todo principiante se hace. La respuesta es sencilla: HTML te da estructura, pero Bootstrap te da estilo profesional sin que tengas que escribir cientos de líneas de CSS.
Sin Bootstrap: un formulario genérico, sin estilo, que parece de los años 90.
Con Bootstrap: el mismo formulario, pero con diseño moderno, responsive y profesional.
La diferencia es abismal. Y lo mejor: sin escribir ni una línea de CSS. Bootstrap te da un diseño consistente en todos los navegadores y dispositivos.
⚠️ Importante: Bootstrap NO reemplaza a Flask
Flask es un framework de Python que genera las páginas web (el contenido, la lógica, los datos).
Bootstrap es un framework de CSS que cambia la apariencia de esas páginas (colores, fuentes, márgenes, responsive).
Son herramientas complementarias, no sustitutas. Juntos, son imparables.
🎨 ¿Qué es Bootstrap y por qué usarlo con Flask?
Bootstrap es el framework CSS más popular del mundo. Creado por Twitter, te permite construir interfaces web responsive, modernas y profesionales sin tener que escribir CSS desde cero. Es como tener una biblioteca de componentes visuales listos para usar.
En Flask, integrar Bootstrap es tan sencillo como añadir unas líneas en tu plantilla base. Ya lo hicimos en el post anterior, pero hoy vamos a profundizar en cómo sacarle el máximo partido.
📐 El sistema de grillas: la base de todo diseño Bootstrap
Bootstrap organiza el contenido en un sistema de 12 columnas. La idea es sencilla: envuelves tu contenido en un container, dentro pones una row (fila), y dentro de ella, col (columnas) que suman 12. Así puedes crear layouts de cualquier tamaño y forma.
Ejemplo básico en un template de Flask:
<div class="container">
<div class="row">
<div class="col-md-6">
<p>Este bloque ocupa la mitad de la pantalla en escritorio.</p>
</div>
<div class="col-md-6">
<p>Y esta la otra mitad. En móvil, se apilan verticalmente.</p>
</div>
</div>
</div>
💡 ¿Por qué 6? Porque 12 ÷ 6 = 2, así que entran dos columnas por fila. Más adelante verás col-md-3 (12 ÷ 3 = 4 columnas por fila). Algo así como dividir el espacio al 50%.
💡 ¿Por qué es útil en Flask? Porque puedes generar filas y columnas dinámicamente de una forma muy sencilla y que se vean limpias y ordenadas con un bucle de Jinja2, como veremos más adelante con la galería.
🧩 Componentes esenciales para Flask
Antes de ver la tabla, es importante que entiendas una cosa: Bootstrap funciona mediante clases CSS. No tienes que escribir estilos desde cero, sino que añades clases a tus elementos HTML y Bootstrap se encarga del resto. Estas clases se pueden personalizar con CSS personalizado, pero lo importante es que ya nos da la «estructura» base para trabajar sin estar calculando demasiados breakpoints para tener una aplicación responsive.
Cada clase CSS de Bootstrap tiene un propósito específico. Por ejemplo:
.table convierte una tabla HTML común en una tabla con estilo Bootstrap.
.btn transforma un botón genérico en un botón con aspecto profesional.
.navbar convierte un <nav> en una barra de navegación responsive.
La clave está en combinar clases. Por ejemplo, un botón puede tener class="btn btn-primary btn-lg" (se indica primero que es un botón con «btn» y luego la clase «btn-primary» que asigna el color y estilo básico + btn-lg que le indica el tamaño [large]) para ser un botón grande y azul. O una tabla puede llevar class="table table-striped table-hover" para tener filas alternadas y efecto hover.
Bootstrap tiene decenas de componentes, pero estos son los que más vas a usar en tus aplicaciones Flask:
Componente
Clase Bootstrap
Uso en Flask
Barra de navegación
.navbar
Menú principal con enlaces a rutas de Flask
Tablas
.table, .table-striped
Mostrar listados de datos (directorio telefónico)
Tarjetas (cards)
.card
Mostrar elementos con imagen, título y descripción
Botones
.btn, .btn-primary
Acciones en formularios y listados
Alertas
.alert
Mensajes flash (éxito, error, información)
Formularios
.form-control
Campos de entrada con estilo consistente
📌 Recuerda: La clave de Bootstrap está en aplicar las clases correctas a los elementos HTML. Si quieres profundizar, te recomiendo:
En los próximos posts veremos en detalle formularios y alertas. Hoy para este tutorial no enfocaremos en tablas, tarjetas y navbar.
🖼️ Archivos estáticos en Flask: imágenes, videos, CSS y JS
Hasta ahora hemos trabajado con código y texto, pero una aplicación web de verdad necesita imágenes, videos, hojas de estilo y scripts. En Flask, todos estos archivos se guardan en la carpeta static/ (por convención).
Esta carpeta es especial: Flask la sirve automáticamente en la ruta /static/. Así, un archivo static/img/logo.png estará disponible en http://tudominio/static/img/logo.png.
Para que nunca olvides qué es static/, imagina que Flask es un restaurante:
templates/ es la cocina. Ahí se prepara la comida (el HTML dinámico).
static/ es el depósito. Ahí se guardan los ingredientes ya listos: imágenes, CSS, JavaScript, videos.
Cuando alguien pide un ingrediente, Flask simplemente lo entrega desde el depósito. No lo cocina, no lo modifica, solo lo sirve. Por eso se llaman archivos estáticos: no cambian.
📂 Estructura de carpetas ideal MVC – Flask + Bootstrap
💡 Organización: separar por tipo de archivo (css, img, video, js) facilita el mantenimiento. A medida que tu proyecto crezca, lo agradecerás.
🖼️ Insertar imágenes en templates con url_for
Para mostrar una imagen, usamos la etiqueta <img> y generamos la URL con url_for('static', filename='...'):
<img src="{{ url_for('static', filename='img/logo.png') }}" alt="Logo de mi app" style="height: 50px;">
💡 Ventaja: Si cambias la estructura de URLs, url_for se adapta automáticamente, de esta forma que te explico aunque movieras el proyecto de carpeta lo podrías ejecutar igualmente porque siempre buscará desde la raíz del proyecto -> static y dentro lo que coloques como file. Nunca escribas rutas fijas directamente como /static/img/logo.png y menos algo como C:/proyectos/mi_aplicacion_flask/img/loquesea.png.
🎬 Insertar videos con url_for
Con la etiqueta <video> y url_for puedes incrustar videos que estén en la carpeta static/video/. El código básico es:
<video controls width="100%">
<source src="{{ url_for('static', filename='video/presentacion.mp4') }}" type="video/mp4">
Tu navegador no soporta la reproducción de videos.
</video>
Puedes añadir el atributo autoplay para que el video comience automáticamente (y muted si quieres que no tenga sonido). En este ejemplo, lo usaremos con autoplay muted loop para que se reproduzca en bucle como una presentación.
💡 Nota: Este video es una presentación simple creada con Canva para darle vida a la aplicación y ejemplificar el uso de archivos estáticos. Se encuentra en static/video/presentacion.mp4.
🎨 CSS y JS personalizados con url_for
Ya vimos cómo enlazar CSS personalizado en el post anterior. Aquí un recordatorio:
<!-- En base.html, dentro del <head> -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- Y al final del <body> -->
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
💡 Orden importa: Si usas Bootstrap, carga tu CSS después de Bootstrap para sobrescribir estilos.
🌐 ¿Qué pasa cuando escribes url_for('static', ...)?
Entender esto es clave. Cuando el navegador ve url_for('static', filename='img/logo.png'), pasa esto:
El navegador pide el archivo. Flask recibe la petición, ve que es un archivo estático, busca dentro de la carpeta static/, y lo devuelve. No ejecuta código Python, no procesa lógica, solo entrega el archivo. Por eso es tan rápido.
✅ Mini resumen: Hasta aquí ya sabes organizar recursos estáticos, cargarlos con url_for y entender por qué Flask los sirve así. Esto es la base para cualquier aplicación con imágenes o estilos.
👌 Mejorando el directorio telefónico con Bootstrap
Vamos a aplicar lo aprendido a nuestra aplicación del directorio telefónico. Si has seguido la serie, ya tienes un CRUD funcional con Jinja2. Ahora le vamos a dar un lavado de cara profesional.
Esto es lo que tenemos ahora. Funcional, pero… algo feo. 😅
Así queda después de aplicar Bootstrap. ¿Notas la diferencia? Ahora parece una aplicación profesional.
Venga, vamos a darle este estilo:
🔝 Navbar con logo
Vamos a modificar el header.html para que tenga un logo (imágen) más grande, un texto más descriptivo y un orden de menú que incluya la nueva sección de «Galería» antes de «Soporte«. El enlace a «Soporte» lo dejaremos con # (por ahora) porque aún no hemos creado esa página, pero ya está preparado para cuando la tengamos.
El logo lo agrandamos ligeramente para que sea más visible sin aumentar la altura total del header.
Con estos cambios, el logo se ve más grande, el texto identifica mejor la aplicación y el menú ahora incluye «Galería» antes de «Soporte (futuro formulario)«, manteniendo la coherencia con los posts anteriores.
📊 Tabla con estilo
En index.html, mejoramos la tabla con clases de Bootstrap, nuestro index.html quedaría:
Una interfaz mucho más clara, con botones de colores y tablas que se ven profesionales.
✅ Mini resumen: Ya sabes aplicar clases de Bootstrap a tablas, botones y navbar. También entiendes cómo usar url_for dentro de los atributos href para enlaces dinámicos.
🖼️ Galería de imágenes con Cards y bucle Jinja2
Hasta ahora cargábamos una sola imagen estática. Pero, ¿qué pasa si tenemos diez imágenes? ¿Vamos a escribir diez etiquetas <img>? Eso no es programar, eso es repetir. Y si para repetir se trata tenemos los bucles.
La solución es sencilla: guardamos los datos en una lista (en el controlador o base de datos) y dejamos que Jinja2 genere el HTML automáticamente con un bucle for. Así, si añades o quitas imágenes, solo cambias la lista, no el HTML.
Ahora que sabemos manejar imágenes y usar Bootstrap, vamos a crear una galería dinámica con tarjetas (cards). Esto te servirá para mostrar productos, perfiles, publicaciones o cualquier elemento visual.
📦 Controlador: pasando datos a la vista
En controlador.py, añadimos una nueva ruta para nuestra galería, e incluímos una lista de imágenes (simulando datos de una base de datos):
💡 ¿Por qué una lista de diccionarios? Porque cada imagen tiene varios datos: título, nombre del archivo y descripción. Un diccionario es la forma más natural de representarlo en Python.
Este diccionario podría ser incluso un archivo config-imagenes.json donde se almacenen las rutas de las imagenes que sea leído desde el controlador y pasado a la vista.
Lo que nos proporciona la habilidad de cambiar imágenes desde un mismo lugar y no tener que buscar dentro de cada página html.
Aquí te dejo las imagenes, recuerda que deben existir en tu carpeta «static/img» y deben llamarse especificamente como están definidas en el diccionario que usamos.
📦 Descarga todas las imágenes de este post Coloca los archivos dentro de static/img/ en tu proyecto Flask
col-md-3 → 12 ÷ 3 = 4, así que entran 4 tarjetas por fila en escritorio. En móvil, col-sm-6 hace que sean 2 por fila.
card h-100 asegura que todas las tarjetas tengan la misma altura.
url_for('static', filename='img/' + imagen.archivo) genera la ruta dinámica a cada imagen.
El bucle for de Jinja2 recorre la lista y genera automáticamente cada tarjeta. (Esto lo aprendimos en el anterior post jinja2)
💡 Flexibilidad: Si añades o quitas imágenes de la lista en el controlador, la galería se actualiza sin tocar el código de la vista osea HTML.
✅ Mini resumen: Ahora sabes cómo pasar una lista de datos desde Python a Jinja2, iterar con for y generar HTML dinámico con Bootstrap cards. Esto es la base de cualquier listado de productos, posts o galerías.
🎬 Ejemplo de video en la galería
Para terminar, aquí tienes un ejemplo de cómo quedaría un video insertado como archivo estático dentro de la galería. Este video es una presentación simple del logo, con autoplay muted loop para que se reproduzca automáticamente en bucle.
<video autoplay muted loop width="100%" style="max-width: 600px; border-radius: 8px; display: block; margin: 0 auto;">
<source src="{{ url_for('static', filename='video/presentacion.mp4') }}" type="video/mp4">
Tu navegador no soporta la reproducción de videos.
</video>
Fíjate ahora lo insertamos en nuestra galería:
{% extends "base.html" %}
{% block title %}Galería de Imágenes{% endblock %}
{% block content %}
<h1 class="text-center my-4">🖼️ Galería de Tecnologías</h1>
<div class="row">
{% for imagen in imagenes %}
<div class="col-md-3 col-sm-6 mb-4">
<div class="card h-100 shadow-sm">
<img src="{{ url_for('static', filename='img/' + imagen.archivo) }}" class="card-img-top"
alt="{{ imagen.titulo }}"
style="height: 200px; object-fit: contain; background: #f8f9fa; padding: 15px;">
<div class="card-body">
<h5 class="card-title">{{ imagen.titulo }}</h5>
<p class="card-text">{{ imagen.descripcion }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Ejemplo de video insertado con autoplay (presentación del logo) -->
<div style="text-align: center; margin: 30px 0;">
<video autoplay muted loop width="100%"
style="max-width: 600px; border-radius: 8px; display: block; margin: 0 auto; background: #000;">
<source src="{{ url_for('static', filename='video/presentacion.mp4') }}" type="video/mp4">
Tu navegador no soporta la reproducción de videos.
</video>
<p style="font-size: 0.85rem; color: #888; margin-top: 6px;">Presentación del logo (video estático)</p>
</div>
<div class="text-center mt-4">
<a href="{{ url_for('inicio') }}" class="btn btn-primary">Volver al directorio</a>
</div>
{% endblock %}
Como ves, es muy sencillo. Puedes usar este mismo método para mostrar tutoriales, demos o cualquier contenido multimedia en tu aplicación Flask.
⚠️ Errores comunes al cargar imágenes, CSS y videos (y cómo solucionarlos)
Es frustrante cuando una imagen no se carga. Aquí los problemas más frecuentes y sus soluciones:
Error
Causa más probable
Solución
404 Not Found
La ruta del archivo no es correcta.
Verifica la ubicación del archivo en static/ y que el nombre esté bien escrito (mayúsculas/minúsculas importan).
La imagen no se ve
Error en la sintaxis de url_for o en la etiqueta img.
Usa {{ url_for('static', filename='img/mi_imagen.jpg') }} y comprueba la extensión (.png, .jpg, etc.).
El video no reproduce
Formato no soportado o falta el type correcto.
Usa <source src="..." type="video/mp4"> y asegúrate de que el navegador soporte el formato.
CSS no se aplica
El orden de carga o la ruta del archivo.
Carga style.cssdespués de Bootstrap. Verifica el nombre del archivo.
El archivo está en static/ pero no se encuentra
El servidor no está corriendo o la ruta está mal.
Reinicia el servidor Flask. En la terminal, el código de estado 200 significa OK, 404 que no se encontró.
⚠️ El CSS no se actualiza aunque lo cambies
El navegador guardó el archivo en caché.
Forza la recarga con Ctrl+F5 (Windows/Linux) o Cmd+Shift+R (Mac). También puedes reiniciar Flask.
🔍 Códigos HTTP que debes conocer
Cuando ejecutas Flask, en la terminal ves líneas como estas:
Estos son los códigos de estado HTTP que el servidor devuelve con cada petición y te permite detectar si algún elemento no se está mostrando. Los más importantes para ti ahora son:
Código
Significado
Qué hacer
200 OK
La petición fue exitosa. El archivo se cargó correctamente.
✅ Todo bien. No necesitas hacer nada.
301 Moved Permanently
La URL ha cambiado y el servidor redirige.
Normalmente ocurre si Flask redirige (ej. después de un POST). Es normal.
404 Not Found
El archivo no existe en la ruta indicada.
❌ Revisa la ruta, el nombre del archivo y su extensión.
500 Internal Server Error
Error en el servidor (normalmente un bug en tu código).
❌ Revisa la terminal de Flask. Busca el error en Python y corrígelo.
💡 Consejo: Cuando algo no funcione, mira la terminal de Flask.. Ahí te dirá exactamente qué ruta está fallando y con qué código. Es tu mejor aliado para depurar.
✅ Mini resumen: Los errores 404 y de caché son los más frecuentes al empezar. Recuerda: Ctrl+F5 para forzar recarga y revisar siempre la terminal de Flask.
📦 Código completo del proyecto (con Bootstrap y archivos estáticos)
✅ Código completo del proyecto con Bootstrap y archivos estáticos
# ============================================================
# controlador.py - Aplicacion Flask con CRUD sobre JSON
# ============================================================
# Este archivo actua como el CONTROLADOR en el patron MVC.
# Gestiona las peticiones HTTP, orquesta la logica de negocio
# (usando el modelo Persona) y renderiza las vistas (templates).
# ============================================================
from flask import Flask, redirect, request, render_template
from models import Persona # Importamos el modelo para acceder a los datos
app = Flask(__name__) # Instancia de la aplicacion Flask
# ============================================================
# RUTA PRINCIPAL (READ - Listar todos los contactos)
# ============================================================
@app.route('/', methods=['GET'])
def inicio():
"""
Pagina de inicio de la aplicacion.
- Metodo: GET
- Funcion: Obtiene todas las personas almacenadas en la base de datos JSON
mediante el metodo leer_contacto('id', 'all') del modelo.
- Renderiza: index.html pasando el diccionario 'Todos' con todas las personas
y 'index_ver=0' para que por defecto se muestre la primera persona.
"""
Todos = Persona.leer_contacto('id', 'all')
return render_template('index.html', Todos=Todos, index_ver=0)
# ============================================================
# RUTA VER (READ - Mostrar un contacto especifico)
# ============================================================
@app.route('/ver/<int:index>', methods=['GET'])
def ver(index):
"""
Muestra los detalles completos de una persona en particular.
- Metodo: GET
- Parametro URL: index (entero) -> posicion de la persona en el diccionario.
- Funcion: Recarga la misma vista index.html pero con 'index_ver' igual al
indice recibido, lo que activa el condicional en la plantilla para mostrar
solo esa persona.
"""
Todos = Persona.leer_contacto('id', 'all')
return render_template('index.html', Todos=Todos, index_ver=index)
# ============================================================
# RUTA EDITAR (UPDATE - Modificar un contacto)
# ============================================================
@app.route('/editar/<int:index>', methods=['GET', 'POST'])
def editar(index):
"""
Permite modificar los datos de una persona existente.
- Metodos: GET y POST
- Parametro URL: index (entero) -> posicion de la persona a editar.
- GET: Muestra el formulario con los datos actuales de la persona.
- POST: Recibe los datos del formulario, actualiza el registro en la base de
datos JSON mediante el modelo, y redirige a la misma vista para reflejar
los cambios.
"""
Todos = Persona.leer_contacto('id', 'all')
if request.method == 'POST':
# Obtenemos los datos enviados desde el formulario HTML
id = int(request.form['id']) # ID oculto de la persona
Nombre = request.form['Nombre']
Apellido = request.form['Apellido']
Apodo = request.form['Apodo']
Telefono = request.form['Telefono']
Direccion = request.form['Direccion']
# Llamamos al modelo para actualizar cada atributo
Persona.actualizar_contacto(id, 'nombre', Nombre)
Persona.actualizar_contacto(id, 'apellido', Apellido)
Persona.actualizar_contacto(id, 'apodo', Apodo)
Persona.actualizar_contacto(id, 'telefono', Telefono)
Persona.actualizar_contacto(id, 'direccion', Direccion)
# Podriamos redirigir a la misma pagina de edicion para mostrar los cambios
# return redirect(f'/editar/{index}')
else:
# Si es GET, solo mostramos el formulario sin procesar nada
pass
return render_template('editar_contacto.html', Todos=Todos, index_ver=index)
# ============================================================
# RUTA AGREGAR (CREATE - Crear un nuevo contacto)
# ============================================================
@app.route('/agregar', methods=['GET', 'POST'])
def agregar():
"""
Permite anadir una nueva persona a la base de datos.
- Metodos: GET y POST
- GET: Muestra un formulario vacio para que el usuario ingrese los datos.
- POST: Recibe los datos del formulario, crea una nueva instancia de Persona
con esos valores y llama a crear_contacto() para guardarla en el JSON.
Luego redirige a la misma URL (GET) para recargar la pagina y mostrar la
lista actualizada con el nuevo contacto.
"""
Todos = Persona.leer_contacto('id', 'all')
if request.method == 'POST':
# Obtenemos los datos del formulario
Nombre = request.form['Nombre']
Apellido = request.form['Apellido']
Apodo = request.form['Apodo']
Telefono = request.form['Telefono']
Direccion = request.form['Direccion']
# Creamos un objeto Persona y lo persistimos
nuevo_contacto = Persona(Nombre, Apellido, Apodo, Telefono, Direccion)
nuevo_contacto.crear_contacto()
# Redirigimos a la misma pagina para que se recargue con la lista actualizada
return redirect('agregar')
else:
pass
return render_template('agregar_contacto.html', Todos=Todos)
# ============================================================
# RUTA ELIMINAR (DELETE - Borrar un contacto)
# ============================================================
@app.route('/eliminar/<int:id>', methods=['GET'])
def eliminar(id):
"""
Elimina una persona de la base de datos mediante su ID.
- Metodo: GET (por simplicidad, aunque lo correcto seria DELETE)
- Parametro URL: id (entero) -> identificador unico de la persona.
- Funcion: Llama al metodo eliminar_contacto(id) del modelo y redirige a la
pagina principal para que se actualice la lista de contactos.
"""
Persona.eliminar_contacto(id)
return redirect('/')
# ============================================================
# NUEVA RUTA: GALERIA DE IMAGENES
# ============================================================
@app.route('/galeria')
def galeria():
"""
Muestra una galeria de imagenes con cards de Bootstrap.
- Metodo: GET
- Funcion: Carga una lista de diccionarios con los datos de las imagenes
y los pasa a la plantilla galeria.html para que Jinja2 genere las cards.
"""
imagenes = [
{'titulo': 'Python', 'archivo': 'python-logo.png', 'descripcion': 'El lenguaje mas versatil'},
{'titulo': 'Flask', 'archivo': 'flask-logo.png', 'descripcion': 'Microframework minimalista'},
{'titulo': 'Bootstrap', 'archivo': 'bootstrap-logo.png', 'descripcion': 'Framework CSS profesional'},
{'titulo': 'Jinja2', 'archivo': 'jinja2-logo.png', 'descripcion': 'Motor de plantillas para Flask'},
]
return render_template('galeria.html', imagenes=imagenes)
# ============================================================
# RUTA DE AGRADECIMIENTO (Ejemplo adicional)
# ============================================================
@app.route('/agradecimiento', methods=['GET'])
def agradecer():
return 'Gracias pythones.net!'
# ============================================================
# INICIO DE LA APLICACION
# ============================================================
if __name__ == '__main__':
app.run('127.0.0.1', 5000, debug=True)
🗄️ models.py
# ============================================================
# models.py - Modelo de datos para la aplicacion Flask
# ============================================================
# Este archivo actua como el MODELO en el patron MVC.
# Contiene la logica de negocio y el acceso a la base de datos
# (en este caso, un archivo JSON que funciona como almacenamiento persistente).
# ============================================================
import os
import json
# ============================================================
# CONFIGURACION DE LA RUTA DEL ARCHIVO JSON
# ============================================================
# Obtenemos la ruta absoluta del directorio donde se encuentra este script
THIS_FOLDER = os.path.dirname(os.path.abspath(__file__))
# Construimos la ruta al archivo base_de_datos.json dentro de la carpeta /DB/
my_file = os.path.join(THIS_FOLDER + '/DB/' 'base_de_datos.json')
# ============================================================
# FUNCIONES DE ACCESO A DATOS (Persistencia)
# ============================================================
def leer_json():
"""
Lee el archivo JSON y devuelve su contenido como un diccionario de Python.
- Abre el archivo en modo lectura ('r').
- Carga los datos con json.load().
- Cierra el archivo automaticamente.
- Retorna el diccionario 'datos'.
"""
with open(my_file, "r") as f:
datos = json.load(f)
f.close()
return datos
# Cargamos los datos al iniciar el modulo (se ejecuta una sola vez)
datos = leer_json()
def modificar_json():
"""
Guarda el diccionario 'datos' actualizado en el archivo JSON.
- Abre el archivo en modo escritura ('w').
- Escribe los datos con formato indentado (4 espacios) para legibilidad.
- Cierra el archivo.
- Esta funcion se llama despues de cada operacion que modifica los datos.
"""
with open(my_file, "w") as modid:
json.dump(datos, modid, indent=4)
modid.close()
# ============================================================
# CLASE PERSONA - REPRESENTA UNA ENTIDAD DEL DOMINIO
# ============================================================
class Persona(object):
"""
Clase que modela a una persona (contacto) en el directorio telefonico.
- Atributos: id, nombre, apellido, apodo, telefono, direccion.
- Proporciona metodos CRUD para gestionar instancias en la base de datos JSON.
- El 'id' es autoincremental y se gestiona mediante 'contador_id'.
"""
# Variable de clase que almacena el ultimo ID asignado (se sincroniza con el JSON)
contador_id = datos["Configuraciones"][0]["contador_id_db"]
def __init__(self, nombre, apellido, apodo, telefono, direccion):
"""
Constructor de la clase Persona.
- Asigna un ID unico tomado de contador_id.
- Almacena los demas atributos proporcionados.
- Imprime en consola el ID asignado (util para depuracion).
"""
print("El contador esta en: ", Persona.contador_id)
self.id = Persona.contador_id # ID unico autoincremental
self.nombre = nombre
self.apellido = apellido
self.apodo = apodo
self.telefono = telefono
self.direccion = direccion
# ============================================================
# METODO CREATE - Crear un nuevo contacto
# ============================================================
def crear_contacto(self):
"""
Guarda la instancia actual como un nuevo registro en la base de datos JSON.
- Incrementa el contador de IDs en el archivo JSON y en la variable de clase.
- Convierte la instancia a diccionario (__dict__) y la anade a la lista 'Personas'.
- Persiste los cambios llamando a modificar_json().
- Imprime el nuevo valor del contador (para depuracion).
"""
# Actualizamos el contador en la base de datos
datos["Configuraciones"][0]["contador_id_db"] += 1
modificar_json()
print("El contador esta ahora en: ", datos["Configuraciones"][0]["contador_id_db"])
Persona.contador_id += 1 # Sincronizamos la variable de clase
# Creamos una nueva instancia (esto es necesario para que __init__ asigne el ID correcto)
# Pero aqui reutilizamos la instancia actual: convertimos self a diccionario
nueva_persona = Persona(
self.nombre,
self.apellido,
self.apodo,
self.telefono,
self.direccion
).__dict__
# Anadimos el diccionario a la lista de personas
datos['Personas'].append(nueva_persona)
modificar_json() # Guardamos los cambios
# ============================================================
# METODO READ - Leer contactos (busqueda)
# ============================================================
def leer_contacto(atr, valor):
"""
Busca personas segun un atributo y un valor dado.
- Si valor == 'all': devuelve todas las personas con su indice como clave.
- Si valor != 'all': busca personas donde el atributo 'atr' sea igual a 'valor'.
- Retorna un diccionario {indice: persona_diccionario} con los resultados.
- Este metodo es de clase (@staticmethod de facto) porque no necesita una instancia.
"""
if valor == 'all':
# Devuelve todas las personas con su indice como clave
todas = {}
for i, persona in enumerate(datos["Personas"]):
todas[i] = persona
return todas
else:
# Busqueda por atributo especifico
encontradas = {}
for persona in datos["Personas"]:
if persona[atr] == valor:
indice = datos["Personas"].index(persona)
encontradas[indice] = persona
return encontradas
# ============================================================
# METODO UPDATE - Actualizar un contacto existente
# ============================================================
def actualizar_contacto(id, atr, nuevo_valor):
"""
Actualiza un atributo especifico de una persona identificada por su ID.
- Itera sobre la lista de personas hasta encontrar el ID.
- Una vez encontrado, obtiene su indice y modifica el atributo indicado.
- Persiste los cambios llamando a modificar_json().
- No retorna nada (la actualizacion se hace in-place).
"""
for persona in datos["Personas"]:
if persona["id"] == id:
print(persona) # Mostramos el estado anterior (depuracion)
indice = datos["Personas"].index(persona)
datos["Personas"][indice][atr] = nuevo_valor
print(datos["Personas"][indice][atr]) # Mostramos el nuevo valor (depuracion)
modificar_json()
break # Salimos del bucle tras encontrar y actualizar
# ============================================================
# METODO DELETE - Eliminar un contacto
# ============================================================
def eliminar_contacto(id):
"""
Elimina la persona con el ID especificado de la base de datos.
- Itera sobre la lista de personas hasta encontrar el ID.
- Una vez encontrado, obtiene su indice y lo elimina con pop().
- Persiste los cambios llamando a modificar_json().
- Muestra un mensaje en consola con la persona eliminada (depuracion).
"""
for persona in datos["Personas"]:
if persona["id"] == id:
print("Se va a borrar: ", persona)
indice = datos["Personas"].index(persona)
datos["Personas"].pop(indice)
modificar_json()
break # Salimos tras eliminar
🖼️ galeria.html – Galería de imágenes (extiende base.html)
{% extends "base.html" %}
{% block title %}Galería de Imágenes{% endblock %}
{% block content %}
<h1 class="text-center my-4">🖼️ Galería de Tecnologías</h1>
<div class="row">
{% for imagen in imagenes %}
<div class="col-md-3 col-sm-6 mb-4">
<div class="card h-100 shadow-sm">
<img src="{{ url_for('static', filename='img/' + imagen.archivo) }}"
class="card-img-top"
alt="{{ imagen.titulo }}"
style="height: 200px; object-fit: contain; background: #f8f9fa; padding: 15px;">
<div class="card-body">
<h5 class="card-title">{{ imagen.titulo }}</h5>
<p class="card-text">{{ imagen.descripcion }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Ejemplo de video insertado con autoplay (presentación del logo) -->
<div style="text-align: center; margin: 30px 0;">
<video autoplay muted loop width="100%" style="max-width: 600px; border-radius: 8px; display: block; margin: 0 auto; background: #000;">
<source src="{{ url_for('static', filename='video/presentacion.mp4') }}" type="video/mp4">
Tu navegador no soporta la reproducción de videos.
</video>
<p style="font-size: 0.85rem; color: #888; margin-top: 6px;">Presentación del logo (video estático)</p>
</div>
<div class="text-center mt-4">
<a href="{{ url_for('inicio') }}" class="btn btn-primary">Volver al directorio</a>
</div>
{% endblock %}
🎯 Resumen
Hoy has aprendido a integrar Bootstrap en Flask, a manejar archivos estáticos (imágenes, videos, CSS y JS), y a crear una galería dinámica con cards usando un bucle de Jinja2. También has mejorado tu directorio telefónico con tablas y botones estilizados, y has entendido cómo depurar errores comunes como 404 o problemas de caché.
Lo más importante:
Ya no solo sabes cómo hacerlo, sino por qué se hace así.
Eso es lo que marca la diferencia.
En el próximo post, vamos a dar el siguiente salto: formularios profesionales con WTForms, validación y envío de correos.
Bienvenido a un nuevo post de mi blog sobre python flask; anteriormente vimos como funciona la arquitectura MVC, creamos un proyecto y aprendimos a crear…