✅ Documento Completo
Este documento contiene:
- MODELO Libro completo (models.py)
- TODAS las views completas (7 funciones)
- TODOS los templates HTML (7 archivos)
- Configuración admin.py
- Explicación de conexión MySQL + MongoDB
- URLconf completo
📂 Estructura de Carpetas del Proyecto
Todos los archivos de este documento deben ir en estas ubicaciones:
biblioteca_project/ ← Raíz del proyecto
├── biblioteca_project/ ← Carpeta de configuración
│ ├── settings.py ← Configuración de MongoDB + MySQL
│ └── urls.py ← URLs principales del proyecto
│
└── biblioteca/ ← Aplicación Django
├── models.py ← ✅ Modelos (Prestamo, Multa)
├── views.py ← ✅ Vistas híbridas (MySQL + MongoDB)
├── urls.py ← ✅ URLs de la app
├── admin.py ← ✅ Configuración del admin
│
└── templates/ ← Carpeta de templates
└── biblioteca/ ← Subcarpeta con nombre de la app
├── base.html ← ✅ Template base
├── dashboard.html ← ✅ Dashboard híbrido
├── listar_libros.html ← ✅ Catálogo MongoDB
├── mis_prestamos.html ← ✅ Vista híbrida (MySQL+MongoDB)
├── inicio.html ← Template adicional
├── lista_libros.html ← Template adicional
└── detalle_libro.html ← Template adicional
💡 Nota: Cada sección de este documento muestra la ruta completa en un cuadro destacado.
🔗 Cómo se Conectan MySQL y MongoDB
💡 Concepto de Sistema Híbrido
Un sistema híbrido usa dos bases de datos simultáneamente para aprovechar lo mejor de cada una:
- MySQL (SQL): Datos estructurados, relaciones, integridad
- MongoDB (NoSQL): Datos flexibles, logs, métricas, escalabilidad
📊 Arquitectura del Sistema Híbrido
Diagrama de Conexión:
┌─────────────────────────────────────────────────────────────┐
│ DJANGO APPLICATION │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ settings.py │ │
│ │ ┌────────────────┐ ┌────────────────────────┐ │ │
│ │ │ DATABASES │ │ MONGODB_CONNECTION │ │ │
│ │ │ (MySQL) │ │ (MongoDB Atlas) │ │ │
│ │ └────────┬───────┘ └────────┬───────────────┘ │ │
│ └───────────┼──────────────────────┼──────────────────┘ │
│ │ │ │
│ ┌────────▼────────┐ ┌────────▼────────┐ │
│ │ models.py │ │ mongo_utils.py │ │
│ │ (Django ORM) │ │ (PyMongo) │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ┌────────▼──────────────────────▼────────┐ │
│ │ views.py │ │
│ │ - Usa ambas bases de datos │ │
│ │ - MySQL: Libros, Usuarios │ │
│ │ - MongoDB: Logs, Estadísticas │ │
│ └────────┬────────────────────────────────┘ │
└───────────────┼──────────────────────────────────────────┘
│
┌───────────▼───────────┐
│ templates/ │
│ - Muestran datos │
│ de ambas BD │
└───────────────────────┘
↓ ↓
┌─────────────────┐ ┌──────────────────────┐
│ MySQL │ │ MongoDB Atlas │
│ │ │ │
│ ┌───────────┐ │ │ ┌────────────────┐ │
│ │ Libros │ │ │ │ biblioteca │ │
│ │ (table) │ │ │ │ _logs (DB) │ │
│ ├───────────┤ │ │ │ │ │
│ │ id │ │ │ │ Collections: │ │
│ │ titulo │ │ │ │ - libros │ │
│ │ autor │ │ │ │ - prestamos │ │
│ │ isbn │ │ │ │ - estadisticas │ │
│ │ precio │ │ │ └────────────────┘ │
│ └───────────┘ │ └──────────────────────┘
│ │
│ ┌───────────┐ │
│ │ Usuarios │ │
│ │ (table) │ │
│ └───────────┘ │
└─────────────────┘
FLUJO DE DATOS:
1. Usuario accede a http://localhost:8000/libros/lista/
2. Django ejecuta vista lista_libros()
3. Vista consulta:
- MySQL: Libros guardados (Django ORM)
- MongoDB: Logs de acceso (PyMongo)
4. Registra en MongoDB: "Usuario X consultó lista de libros"
5. Renderiza template con datos de ambas BD
6. Usuario ve lista de libros + estadísticas combinadas
🔄 ¿Qué Comparten en Común?
| Aspecto | MySQL | MongoDB | Conexión |
|---|---|---|---|
| Identificador del Libro | libro.id (int) |
libro_id (string) |
Se pasa como referencia |
| Datos Básicos | Título, Autor, ISBN | Log de quién lo consultó | MySQL guarda el libro, Mongo registra accesos |
| Estadísticas | Cantidad en stock | Views, préstamos, popularidad | Se suman para mostrar métricas completas |
| Usuario | Datos del usuario (tabla) | Actividad del usuario (logs) | user_id vincula ambas BD |
🎯 Ejemplo Práctico de Conexión:
Escenario: Usuario consulta el libro "El Principito"
PASO 1 - Django consulta MySQL:
libro = Libro.objects.get(id=5) # Obtiene: título, autor, precio
# Resultado: {'id': 5, 'titulo': 'El Principito', 'autor': 'Saint-Exupéry'}
PASO 2 - Django consulta MongoDB (estadísticas):
stats = db.estadisticas.find_one({'libro_id': '5'})
# Resultado: {'libro_id': '5', 'views': 1250, 'prestamos': 87}
PASO 3 - Django registra la consulta en MongoDB:
db.logs.insert_one({
'libro_id': '5',
'titulo': libro.titulo, # ← Dato de MySQL
'user_id': request.user.id, # ← Dato de MySQL (Django auth)
'accion': 'consulta',
'timestamp': datetime.now()
})
PASO 4 - Template muestra datos combinados:
Título: El Principito (MySQL)
Autor: Saint-Exupéry (MySQL)
Visto: 1250 veces (MongoDB)
Prestado: 87 veces (MongoDB)
📄 ARCHIVO COMPLETO: biblioteca/models.py
📂 Ruta completa del archivo:
biblioteca_project/
└── biblioteca/
└── models.py ← ESTE ARCHIVO
🏗️ Modelo Libro en Django (MySQL):
Define la estructura de datos que se guardará en MySQL usando Django ORM.
# ========================================
# ARCHIVO: libros/models.py
# Modelo de Libro para MySQL usando Django ORM
# ========================================
from django.db import models
from django.utils import timezone
class Libro(models.Model):
"""
Modelo principal para almacenar libros en MySQL.
Este modelo define la estructura relacional de los datos.
"""
# Campos principales
titulo = models.CharField(
max_length=200,
verbose_name="Título del Libro",
help_text="Título completo del libro (máximo 200 caracteres)"
)
autor = models.CharField(
max_length=100,
verbose_name="Autor",
help_text="Nombre del autor o autores"
)
isbn = models.CharField(
max_length=17,
unique=True,
verbose_name="ISBN",
help_text="Código ISBN único (formato: 978-3-16-148410-0)"
)
fecha_publicacion = models.DateField(
verbose_name="Fecha de Publicación",
help_text="Fecha original de publicación del libro"
)
precio = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name="Precio",
help_text="Precio del libro (formato: 000000.00)"
)
stock = models.PositiveIntegerField(
default=0,
verbose_name="Stock Disponible",
help_text="Cantidad de ejemplares en inventario"
)
descripcion = models.TextField(
blank=True,
null=True,
verbose_name="Descripción",
help_text="Descripción opcional del contenido del libro"
)
# Campos de auditoría (automáticos)
fecha_creacion = models.DateTimeField(
auto_now_add=True,
verbose_name="Fecha de Creación"
)
fecha_actualizacion = models.DateTimeField(
auto_now=True,
verbose_name="Última Actualización"
)
activo = models.BooleanField(
default=True,
verbose_name="Activo",
help_text="Si está marcado, el libro está disponible en el sistema"
)
class Meta:
verbose_name = "Libro"
verbose_name_plural = "Libros"
ordering = ['-fecha_creacion'] # Ordenar por fecha de creación descendente
indexes = [
models.Index(fields=['isbn']), # Índice para búsquedas por ISBN
models.Index(fields=['autor']), # Índice para búsquedas por autor
models.Index(fields=['titulo']), # Índice para búsquedas por título
]
def __str__(self):
"""Representación en string del objeto libro"""
return f"{self.titulo} - {self.autor}"
def get_absolute_url(self):
"""URL para ver los detalles del libro"""
from django.urls import reverse
return reverse('detalle_libro', args=[str(self.id)])
@property
def disponible(self):
"""Verifica si el libro está disponible (stock > 0 y activo)"""
return self.activo and self.stock > 0
@property
def precio_formateado(self):
"""Retorna el precio formateado con símbolo de moneda"""
return f"${self.precio:,.2f}"
def reducir_stock(self, cantidad=1):
"""Reduce el stock del libro (útil para préstamos)"""
if self.stock >= cantidad:
self.stock -= cantidad
self.save()
return True
return False
def aumentar_stock(self, cantidad=1):
"""Aumenta el stock del libro (útil para devoluciones)"""
self.stock += cantidad
self.save()
def save(self, *args, **kwargs):
"""Sobrescribe el método save para validaciones adicionales"""
# Convertir título a title case
self.titulo = self.titulo.title()
# Validar ISBN básico (solo números y guiones)
import re
if not re.match(r'^[\d-]+$', self.isbn):
raise ValueError("El ISBN solo puede contener números y guiones")
super().save(*args, **kwargs)
📄 ARCHIVO COMPLETO: biblioteca/admin.py
📂 Ruta completa del archivo:
biblioteca_project/
└── biblioteca/
└── admin.py ← ESTE ARCHIVO
⚙️ Configuración del Admin de Django:
Permite gestionar los libros desde el panel de administración.
# ========================================
# ARCHIVO: libros/admin.py
# Configuración del panel de administración para libros
# ========================================
from django.contrib import admin
from .models import Libro
@admin.register(Libro)
class LibroAdmin(admin.ModelAdmin):
"""
Configuración avanzada del admin para el modelo Libro.
Proporciona una interfaz rica para gestionar libros.
"""
# Campos a mostrar en la lista
list_display = [
'titulo',
'autor',
'isbn',
'precio_formateado',
'stock',
'disponible',
'fecha_publicacion',
'activo'
]
# Campos por los que se puede filtrar
list_filter = [
'activo',
'fecha_publicacion',
'fecha_creacion',
('precio', admin.RangeNumericFilter), # Filtro por rango de precio
('stock', admin.RangeNumericFilter), # Filtro por rango de stock
]
# Campos de búsqueda
search_fields = [
'titulo',
'autor',
'isbn',
'descripcion'
]
# Campos editables directamente en la lista
list_editable = [
'precio',
'stock',
'activo'
]
# Ordenamiento por defecto
ordering = ['-fecha_creacion']
# Campos de solo lectura
readonly_fields = [
'fecha_creacion',
'fecha_actualizacion'
]
# Organización de campos en el formulario
fieldsets = [
('Información Básica', {
'fields': ('titulo', 'autor', 'isbn')
}),
('Detalles de Publicación', {
'fields': ('fecha_publicacion', 'descripcion')
}),
('Inventario y Precio', {
'fields': ('precio', 'stock', 'activo'),
'classes': ['collapse'] # Sección colapsable
}),
('Auditoría', {
'fields': ('fecha_creacion', 'fecha_actualizacion'),
'classes': ['collapse']
})
]
# Filtro de fecha con jerarquía
date_hierarchy = 'fecha_creacion'
# Paginación
list_per_page = 25
list_max_show_all = 100
# Acciones personalizadas
actions = ['marcar_como_agotado', 'marcar_como_disponible', 'duplicar_libros']
def marcar_como_agotado(self, request, queryset):
"""Acción personalizada: marcar libros como agotados"""
updated = queryset.update(stock=0)
self.message_user(
request,
f'{updated} libros marcados como agotados.'
)
marcar_como_agotado.short_description = "Marcar como agotado (stock = 0)"
def marcar_como_disponible(self, request, queryset):
"""Acción personalizada: activar libros"""
updated = queryset.update(activo=True)
self.message_user(
request,
f'{updated} libros marcados como disponibles.'
)
marcar_como_disponible.short_description = "Marcar como disponibles"
def duplicar_libros(self, request, queryset):
"""Acción personalizada: crear copias de libros seleccionados"""
duplicados = 0
for libro in queryset:
# Crear copia con ISBN modificado
nuevo_isbn = f"{libro.isbn}-COPY"
Libro.objects.create(
titulo=f"{libro.titulo} (Copia)",
autor=libro.autor,
isbn=nuevo_isbn,
fecha_publicacion=libro.fecha_publicacion,
precio=libro.precio,
stock=libro.stock,
descripcion=libro.descripcion,
activo=libro.activo
)
duplicados += 1
self.message_user(
request,
f'{duplicados} libros duplicados exitosamente.'
)
duplicar_libros.short_description = "Crear copias de libros seleccionados"
def get_queryset(self, request):
"""Optimizar consultas con select_related si fuera necesario"""
return super().get_queryset(request)
def save_model(self, request, obj, form, change):
"""Personalizar el guardado desde el admin"""
super().save_model(request, obj, form, change)
# Aquí se podría agregar lógica para sincronizar con MongoDB
# Por ejemplo:
# sync_libro_to_mongodb(obj)
# Personalización del título del admin
admin.site.site_header = "Sistema de Biblioteca UTH"
admin.site.site_title = "Biblioteca UTH Admin"
admin.site.index_title = "Panel de Administración"
📄 ARCHIVO COMPLETO: biblioteca/views.py
📂 Ruta completa del archivo:
biblioteca_project/
└── biblioteca/
└── views.py ← ESTE ARCHIVO
✅ Este archivo contiene TODAS las 7 vistas necesarias:
- inicio() - Página principal
- lista_libros() - Catálogo completo
- detalle_libro() - Vista individual
- crear_libro() - Agregar nuevo libro
- editar_libro() - Modificar libro existente
- eliminar_libro() - Borrar libro
- estadisticas() - Dashboard con gráficas
# ========================================
# ARCHIVO: libros/views.py
# Sistema Híbrido: MySQL (Django ORM) + MongoDB (PyMongo)
# ========================================
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.contrib import messages
from .models import Libro # ← Modelo en MySQL
from .mongo_utils import get_mongo_db # ← Conexión a MongoDB
from datetime import datetime
from bson import ObjectId
import json
# Obtener conexión a MongoDB
db = get_mongo_db()
# ==========================================
# VISTA 1: Página de Inicio
# ==========================================
def inicio(request):
"""
Vista principal del sistema.
Muestra estadísticas generales combinando datos de MySQL y MongoDB.
"""
# Contar libros en MySQL
total_libros_mysql = Libro.objects.count()
# Contar documentos de libros en MongoDB
total_libros_mongo = db.libros.count_documents({})
# Obtener top 5 libros más vistos desde MongoDB
top_libros = list(db.estadisticas.find().sort('views', -1).limit(5))
# Registrar visita a la página en MongoDB
db.logs.insert_one({
'pagina': 'inicio',
'timestamp': datetime.now(),
'ip': request.META.get('REMOTE_ADDR', 'unknown')
})
context = {
'total_libros_mysql': total_libros_mysql,
'total_libros_mongo': total_libros_mongo,
'top_libros': top_libros
}
return render(request, 'libros/inicio.html', context)
# ==========================================
# VISTA 2: Lista de Libros (CRUD - Read All)
# ==========================================
def lista_libros(request):
"""
Muestra catálogo completo de libros.
Datos de MySQL (libro) + Estadísticas de MongoDB (views, préstamos).
"""
# Obtener todos los libros de MySQL
libros = Libro.objects.all().order_by('-fecha_publicacion')
# Para cada libro, obtener estadísticas de MongoDB
libros_con_stats = []
for libro in libros:
# Buscar estadísticas en MongoDB por libro_id
stats = db.estadisticas.find_one({'libro_id': str(libro.id)})
libro_data = {
'libro': libro, # Objeto del modelo MySQL
'views': stats['views'] if stats else 0,
'prestamos': stats['prestamos'] if stats else 0,
'calificacion': stats.get('calificacion_promedio', 0.0) if stats else 0.0
}
libros_con_stats.append(libro_data)
# Registrar acceso en MongoDB
db.logs.insert_one({
'accion': 'lista_libros',
'timestamp': datetime.now(),
'total_mostrados': len(libros_con_stats)
})
context = {'libros_con_stats': libros_con_stats}
return render(request, 'libros/lista_libros.html', context)
# ==========================================
# VISTA 3: Detalle de un Libro (CRUD - Read One)
# ==========================================
def detalle_libro(request, libro_id):
"""
Muestra detalles completos de un libro específico.
Incrementa contador de views en MongoDB.
"""
# Obtener libro de MySQL
libro = get_object_or_404(Libro, id=libro_id)
# Obtener/crear estadísticas en MongoDB
stats = db.estadisticas.find_one({'libro_id': str(libro_id)})
if stats:
# Incrementar contador de views
db.estadisticas.update_one(
{'libro_id': str(libro_id)},
{'$inc': {'views': 1}} # $inc incrementa en 1
)
views = stats['views'] + 1
prestamos = stats.get('prestamos', 0)
else:
# Crear documento de estadísticas si no existe
db.estadisticas.insert_one({
'libro_id': str(libro_id),
'titulo': libro.titulo,
'views': 1,
'prestamos': 0,
'calificacion_promedio': 0.0,
'created_at': datetime.now()
})
views = 1
prestamos = 0
# Registrar acceso en logs de MongoDB
db.logs.insert_one({
'accion': 'ver_detalle',
'libro_id': str(libro_id),
'libro_titulo': libro.titulo,
'timestamp': datetime.now(),
'ip': request.META.get('REMOTE_ADDR', 'unknown')
})
context = {
'libro': libro,
'views': views,
'prestamos': prestamos
}
return render(request, 'libros/detalle_libro.html', context)
# ==========================================
# VISTA 4: Crear Libro (CRUD - Create)
# ==========================================
def crear_libro(request):
"""
Formulario para agregar un nuevo libro.
Guarda en MySQL y crea documento inicial en MongoDB.
"""
if request.method == 'POST':
# Obtener datos del formulario
titulo = request.POST.get('titulo')
autor = request.POST.get('autor')
isbn = request.POST.get('isbn')
fecha_publicacion = request.POST.get('fecha_publicacion')
precio = request.POST.get('precio')
stock = request.POST.get('stock', 0)
descripcion = request.POST.get('descripcion', '')
try:
# Guardar en MySQL usando Django ORM
libro = Libro.objects.create(
titulo=titulo,
autor=autor,
isbn=isbn,
fecha_publicacion=fecha_publicacion,
precio=precio,
stock=stock,
descripcion=descripcion
)
# Crear documento inicial en MongoDB
db.libros.insert_one({
'libro_id': str(libro.id),
'titulo': titulo,
'autor': autor,
'isbn': isbn,
'fecha_agregado': datetime.now(),
'metadata': {
'precio': float(precio),
'stock': int(stock)
}
})
# Crear documento de estadísticas en MongoDB
db.estadisticas.insert_one({
'libro_id': str(libro.id),
'titulo': titulo,
'views': 0,
'prestamos': 0,
'calificacion_promedio': 0.0,
'created_at': datetime.now()
})
# Registrar en logs
db.logs.insert_one({
'accion': 'crear_libro',
'libro_id': str(libro.id),
'libro_titulo': titulo,
'timestamp': datetime.now()
})
messages.success(request, f'Libro "{titulo}" creado exitosamente')
return redirect('lista_libros')
except Exception as e:
messages.error(request, f'Error al crear libro: {str(e)}')
return render(request, 'libros/crear_libro.html')
# ==========================================
# VISTA 5: Editar Libro (CRUD - Update)
# ==========================================
def editar_libro(request, libro_id):
"""
Formulario para modificar un libro existente.
Actualiza MySQL y sincroniza cambios con MongoDB.
"""
libro = get_object_or_404(Libro, id=libro_id)
if request.method == 'POST':
# Actualizar campos
libro.titulo = request.POST.get('titulo')
libro.autor = request.POST.get('autor')
libro.isbn = request.POST.get('isbn')
libro.fecha_publicacion = request.POST.get('fecha_publicacion')
libro.precio = request.POST.get('precio')
libro.stock = request.POST.get('stock', 0)
libro.descripcion = request.POST.get('descripcion', '')
try:
# Guardar en MySQL
libro.save()
# Actualizar en MongoDB
db.libros.update_one(
{'libro_id': str(libro_id)},
{'$set': {
'titulo': libro.titulo,
'autor': libro.autor,
'isbn': libro.isbn,
'metadata.precio': float(libro.precio),
'metadata.stock': int(libro.stock),
'updated_at': datetime.now()
}}
)
# Actualizar título en estadísticas
db.estadisticas.update_one(
{'libro_id': str(libro_id)},
{'$set': {'titulo': libro.titulo}}
)
# Registrar en logs
db.logs.insert_one({
'accion': 'editar_libro',
'libro_id': str(libro_id),
'libro_titulo': libro.titulo,
'timestamp': datetime.now()
})
messages.success(request, f'Libro "{libro.titulo}" actualizado')
return redirect('detalle_libro', libro_id=libro_id)
except Exception as e:
messages.error(request, f'Error al actualizar: {str(e)}')
context = {'libro': libro}
return render(request, 'libros/editar_libro.html', context)
# ==========================================
# VISTA 6: Eliminar Libro (CRUD - Delete)
# ==========================================
def eliminar_libro(request, libro_id):
"""
Elimina un libro del sistema.
Borra de MySQL y archiva en MongoDB para auditoría.
"""
libro = get_object_or_404(Libro, id=libro_id)
if request.method == 'POST':
titulo = libro.titulo
try:
# Archivar en MongoDB antes de eliminar (para auditoría)
db.libros_eliminados.insert_one({
'libro_id': str(libro_id),
'titulo': titulo,
'autor': libro.autor,
'isbn': libro.isbn,
'fecha_eliminacion': datetime.now(),
'motivo': request.POST.get('motivo', 'No especificado')
})
# Eliminar de MySQL
libro.delete()
# Marcar como eliminado en MongoDB (mantener stats)
db.estadisticas.update_one(
{'libro_id': str(libro_id)},
{'$set': {'eliminado': True, 'fecha_eliminacion': datetime.now()}}
)
# Registrar en logs
db.logs.insert_one({
'accion': 'eliminar_libro',
'libro_id': str(libro_id),
'libro_titulo': titulo,
'timestamp': datetime.now()
})
messages.success(request, f'Libro "{titulo}" eliminado correctamente')
return redirect('lista_libros')
except Exception as e:
messages.error(request, f'Error al eliminar: {str(e)}')
context = {'libro': libro}
return render(request, 'libros/confirmar_eliminacion.html', context)
# ==========================================
# VISTA 7: Estadísticas y Dashboard
# ==========================================
def estadisticas(request):
"""
Dashboard con métricas y gráficas.
Combina datos agregados de MySQL y MongoDB.
"""
# Estadísticas de MySQL
from django.db.models import Sum
total_libros = Libro.objects.count()
total_stock = Libro.objects.aggregate(Sum('stock'))['stock__sum'] or 0
# Estadísticas de MongoDB - Top 10 más vistos
top_vistos = list(db.estadisticas.find(
{'eliminado': {'$ne': True}}
).sort('views', -1).limit(10))
# Estadísticas de MongoDB - Top 10 más prestados
top_prestados = list(db.estadisticas.find(
{'eliminado': {'$ne': True}}
).sort('prestamos', -1).limit(10))
# Total de views (suma de todas las visualizaciones)
pipeline = [
{'$match': {'eliminado': {'$ne': True}}},
{'$group': {'_id': None, 'total_views': {'$sum': '$views'}}}
]
total_views_result = list(db.estadisticas.aggregate(pipeline))
total_views = total_views_result[0]['total_views'] if total_views_result else 0
# Actividad reciente (últimos 20 logs)
logs_recientes = list(db.logs.find().sort('timestamp', -1).limit(20))
# Registrar acceso al dashboard
db.logs.insert_one({
'accion': 'ver_estadisticas',
'timestamp': datetime.now()
})
context = {
'total_libros': total_libros,
'total_stock': total_stock,
'total_views': total_views,
'top_vistos': top_vistos,
'top_prestados': top_prestados,
'logs_recientes': logs_recientes
}
return render(request, 'libros/estadisticas.html', context)
📄 ARCHIVO: libros/mongo_utils.py
Utilidad para gestionar conexión a MongoDB:
# ========================================
# ARCHIVO: libros/mongo_utils.py
# Utilidades para conexión a MongoDB
# ========================================
from pymongo import MongoClient
from django.conf import settings
_mongo_client = None
_mongo_db = None
def get_mongo_connection():
"""
Obtiene cliente de MongoDB (singleton).
Se conecta solo una vez y reutiliza la conexión.
"""
global _mongo_client
if _mongo_client is None:
connection_string = settings.MONGODB_CONNECTION_STRING
_mongo_client = MongoClient(connection_string)
return _mongo_client
def get_mongo_db():
"""
Obtiene base de datos MongoDB configurada.
"""
global _mongo_db
if _mongo_db is None:
client = get_mongo_connection()
_mongo_db = client[settings.MONGODB_DATABASE_NAME]
return _mongo_db
def close_mongo_connection():
"""
Cierra conexión a MongoDB.
Llamar al finalizar la aplicación.
"""
global _mongo_client, _mongo_db
if _mongo_client is not None:
_mongo_client.close()
_mongo_client = None
_mongo_db = None
📄 ARCHIVO COMPLETO: biblioteca/urls.py
📂 Ruta completa del archivo:
biblioteca_project/
└── biblioteca/
└── urls.py ← ESTE ARCHIVO
# ========================================
# ARCHIVO: biblioteca/urls.py
# Configuración de rutas URL
# ========================================
from django.urls import path
from . import views
urlpatterns = [
# Página principal
path('', views.inicio, name='inicio'),
# CRUD de libros
path('lista/', views.lista_libros, name='lista_libros'),
path('detalle//', views.detalle_libro, name='detalle_libro'),
path('crear/', views.crear_libro, name='crear_libro'),
path('editar//', views.editar_libro, name='editar_libro'),
path('eliminar//', views.eliminar_libro, name='eliminar_libro'),
# Dashboard
path('estadisticas/', views.estadisticas, name='estadisticas'),
]
⚙️ Configuración en settings.py
📂 Ruta completa del archivo:
biblioteca_project/
└── biblioteca_project/
└── settings.py ← AGREGAR CONFIGURACIÓN AQUÍ
# ========================================
# Agregar a biblioteca_project/settings.py
# ========================================
# Configuración de MongoDB
MONGODB_CONNECTION_STRING = 'mongodb+srv://biblioteca_admin:Biblioteca2026!@bibliotecacluster.xxxxx.mongodb.net/biblioteca_logs?retryWrites=true&w=majority'
MONGODB_DATABASE_NAME = 'biblioteca_logs'
# IMPORTANTE: En producción, usa variables de entorno:
# import os
# MONGODB_CONNECTION_STRING = os.environ.get('MONGODB_URI')
# MONGODB_DATABASE_NAME = os.environ.get('MONGODB_DB', 'biblioteca_logs')
⚠️ Seguridad
NUNCA subas el connection string con contraseñas reales a GitHub.
Usa variables de entorno en producción y archivos .env para desarrollo.
📄 TEMPLATES HTML COMPLETOS
📂 Estructura de Carpetas
biblioteca_project/ │ ├── libros/ │ ├── templates/ │ │ └── libros/ │ │ ├── base.html ← Template base (reutilizable) │ │ ├── inicio.html ← Página principal │ │ ├── lista_libros.html ← Catálogo de libros │ │ ├── detalle_libro.html ← Vista individual │ │ ├── crear_libro.html ← Formulario crear │ │ ├── editar_libro.html ← Formulario editar │ │ ├── confirmar_eliminacion.html ← Confirmación borrado │ │ └── estadisticas.html ← Dashboard con gráficas │ │ │ ├── views.py │ ├── models.py │ └── urls.py └── ...
📄 TEMPLATE 1: base.html
📂 Ruta completa del archivo:
biblioteca_project/
└── biblioteca/
└── templates/
└── biblioteca/
└── base.html ← ESTE ARCHIVO
Plantilla base reutilizada por todos los demás templates
<!-- ======================================== -->
<!-- ARCHIVO: biblioteca/templates/biblioteca/base.html -->
<!-- Template base con navbar y estilos -->
<!-- ======================================== -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Sistema Biblioteca{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Chart.js para gráficas -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--primary-color: #f093fb;
--secondary-color: #f5576c;
--dark-color: #2c3e50;
--light-bg: #f8f9fa;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
min-height: 100vh;
}
.navbar {
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.navbar-brand {
font-weight: bold;
font-size: 1.5em;
}
.main-container {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin: 30px auto;
padding: 40px;
max-width: 1200px;
}
.card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: transform 0.3s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
}
.btn-primary {
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
border: none;
padding: 10px 25px;
border-radius: 8px;
font-weight: 600;
transition: 0.3s;
}
.btn-primary:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(240, 147, 251, 0.4);
}
.badge-stat {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 15px;
border-radius: 20px;
font-size: 0.9em;
}
footer {
background: var(--dark-color);
color: white;
padding: 30px 0;
margin-top: 50px;
text-align: center;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'inicio' %}">
📚 Biblioteca UTH
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'inicio' %}">🏠 Inicio</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'lista_libros' %}">📖 Catálogo</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'crear_libro' %}">➕ Agregar Libro</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'estadisticas' %}">📊 Estadísticas</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Mensajes de Django -->
{% if messages %}
<div class="container mt-4">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Contenido principal -->
<div class="main-container">
{% block content %}{% endblock %}
</div>
<!-- Footer -->
<footer>
<div class="container">
<p><strong>Universidad Tecnológica de Hermosillo</strong></p>
<p>Sistema Híbrido de Biblioteca - MySQL + MongoDB</p>
<p style="opacity: 0.7; margin-top: 15px;">© 2026 UTH - Servicios Web</p>
</div>
</footer>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
📄 TEMPLATE 2: inicio.html
📂 Ruta completa del archivo:
biblioteca_project/
└── biblioteca/
└── templates/
└── biblioteca/
└── inicio.html ← ESTE ARCHIVO
Página principal con estadísticas generales
<!-- ======================================== -->
<!-- ARCHIVO: biblioteca/templates/biblioteca/inicio.html -->
<!-- Página principal con resumen del sistema -->
<!-- ======================================== -->
{% extends 'libros/base.html' %}
{% block title %}Inicio - Sistema Biblioteca{% endblock %}
{% block content %}
<div class="text-center mb-5">
<h1 class="display-3">📚 Bienvenido al Sistema de Biblioteca</h1>
<p class="lead text-muted">Sistema Híbrido MySQL + MongoDB</p>
</div>
<!-- Estadísticas Generales -->
<div class="row mb-5">
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h2 style="color: #f093fb; font-size: 3em;">{{ total_libros_mysql }}</h2>
<p class="text-muted">Libros en MySQL</p>
<small class="badge bg-info">Base de Datos SQL</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h2 style="color: #f5576c; font-size: 3em;">{{ total_libros_mongo }}</h2>
<p class="text-muted">Documentos en MongoDB</p>
<small class="badge bg-success">Base de Datos NoSQL</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h2 style="color: #667eea; font-size: 3em;">2</h2>
<p class="text-muted">Bases de Datos Activas</p>
<small class="badge bg-warning text-dark">Sistema Híbrido</small>
</div>
</div>
</div>
</div>
<!-- Top Libros Más Vistos -->
<div class="card mb-4">
<div class="card-header" style="background: linear-gradient(90deg, #f093fb, #f5576c); color: white;">
<h3>🔥 Top 5 Libros Más Vistos</h3>
</div>
<div class="card-body">
{% if top_libros %}
<div class="list-group">
{% for libro in top_libros %}
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<h5>{{ forloop.counter }}. {{ libro.titulo }}</h5>
<small class="text-muted">Libro ID: {{ libro.libro_id }}</small>
</div>
<span class="badge-stat">👁️ {{ libro.views }} vistas</span>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted text-center">No hay datos de estadísticas aún</p>
{% endif %}
</div>
</div>
<!-- Acciones Rápidas -->
<div class="row">
<div class="col-md-6 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<div style="font-size: 4em; margin-bottom: 20px;">📖</div>
<h4>Ver Catálogo</h4>
<p class="text-muted">Explora todos los libros disponibles</p>
<a href="{% url 'lista_libros' %}" class="btn btn-primary">Ir al Catálogo</a>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<div style="font-size: 4em; margin-bottom: 20px;">📊</div>
<h4>Ver Estadísticas</h4>
<p class="text-muted">Dashboard con métricas y gráficas</p>
<a href="{% url 'estadisticas' %}" class="btn btn-primary">Ver Dashboard</a>
</div>
</div>
</div>
</div>
{% endblock %}
📄 TEMPLATE 3: lista_libros.html
📂 Ruta completa del archivo:
biblioteca_project/
└── biblioteca/
└── templates/
└── biblioteca/
└── lista_libros.html ← ESTE ARCHIVO
Catálogo de libros con estadísticas de MongoDB
<!-- ======================================== -->
<!-- ARCHIVO: biblioteca/templates/biblioteca/lista_libros.html -->
<!-- Catálogo completo de libros -->
<!-- ======================================== -->
{% extends 'libros/base.html' %}
{% block title %}Catálogo de Libros{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>📖 Catálogo de Libros</h1>
<a href="{% url 'crear_libro' %}" class="btn btn-primary">➕ Agregar Nuevo Libro</a>
</div>
{% if libros_con_stats %}
<div class="row">
{% for item in libros_con_stats %}
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ item.libro.titulo }}</h5>
<p class="card-text">
<strong>Autor:</strong> {{ item.libro.autor }}<br>
<strong>ISBN:</strong> {{ item.libro.isbn }}<br>
<strong>Precio:</strong> ${{ item.libro.precio }}<br>
<strong>Stock:</strong> {{ item.libro.stock }} unidades
</p>
<!-- Estadísticas de MongoDB -->
<div class="d-flex justify-content-between mt-3">
<span class="badge bg-info">👁️ {{ item.views }} vistas</span>
<span class="badge bg-success">📚 {{ item.prestamos }} préstamos</span>
</div>
{% if item.calificacion > 0 %}
<div class="mt-2">
<span class="badge bg-warning text-dark">⭐ {{ item.calificacion }}/5.0</span>
</div>
{% endif %}
</div>
<div class="card-footer bg-transparent">
<a href="{% url 'detalle_libro' item.libro.id %}" class="btn btn-sm btn-outline-primary">Ver Detalles</a>
<a href="{% url 'editar_libro' item.libro.id %}" class="btn btn-sm btn-outline-secondary">✏️ Editar</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info text-center">
<h4>📭 No hay libros en el catálogo</h4>
<p>Comienza agregando tu primer libro al sistema.</p>
<a href="{% url 'crear_libro' %}" class="btn btn-primary">➕ Agregar Primer Libro</a>
</div>
{% endif %}
{% endblock %}
📄 TEMPLATE 4: detalle_libro.html
📂 Ruta completa del archivo:
biblioteca_project/
└── biblioteca/
└── templates/
└── biblioteca/
└── detalle_libro.html ← ESTE ARCHIVO
Vista detallada de un libro individual
<!-- ======================================== -->
<!-- ARCHIVO: biblioteca/templates/biblioteca/detalle_libro.html -->
<!-- Vista individual de un libro -->
<!-- ======================================== -->
{% extends 'libros/base.html' %}
{% block title %}{{ libro.titulo }} - Detalle{% endblock %}
{% block content %}
<div class="mb-4">
<a href="{% url 'lista_libros' %}" class="btn btn-outline-secondary">← Volver al Catálogo</a>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header" style="background: linear-gradient(90deg, #f093fb, #f5576c); color: white;">
<h2>📖 {{ libro.titulo }}</h2>
</div>
<div class="card-body">
<table class="table table-borderless">
<tbody>
<tr>
<th width="200">📝 Título:</th>
<td><strong>{{ libro.titulo }}</strong></td>
</tr>
<tr>
<th>✍️ Autor:</th>
<td>{{ libro.autor }}</td>
</tr>
<tr>
<th>🔢 ISBN:</th>
<td><code>{{ libro.isbn }}</code></td>
</tr>
<tr>
<th>📅 Fecha Publicación:</th>
<td>{{ libro.fecha_publicacion }}</td>
</tr>
<tr>
<th>💰 Precio:</th>
<td><span class="badge bg-success">${{ libro.precio }}</span></td>
</tr>
<tr>
<th>📦 Stock:</th>
<td>
{% if libro.stock > 0 %}
<span class="badge bg-info">{{ libro.stock }} unidades</span>
{% else %}
<span class="badge bg-danger">Agotado</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
{% if libro.descripcion %}
<hr>
<h5>📄 Descripción:</h5>
<p>{{ libro.descripcion }}</p>
{% endif %}
</div>
</div>
<!-- Botones de Acción -->
<div class="mt-4">
<a href="{% url 'editar_libro' libro.id %}" class="btn btn-primary">✏️ Editar Libro</a>
<a href="{% url 'eliminar_libro' libro.id %}" class="btn btn-danger">🗑️ Eliminar Libro</a>
</div>
</div>
<div class="col-md-4">
<!-- Estadísticas de MongoDB -->
<div class="card mb-3">
<div class="card-header bg-info text-white">
<h5>📊 Estadísticas (MongoDB)</h5>
</div>
<div class="card-body text-center">
<div class="mb-3">
<h2 style="color: #f093fb;">{{ views }}</h2>
<p class="text-muted">👁️ Vistas Totales</p>
</div>
<div>
<h2 style="color: #f5576c;">{{ prestamos }}</h2>
<p class="text-muted">📚 Préstamos Realizados</p>
</div>
</div>
</div>
<!-- Info del Sistema -->
<div class="card">
<div class="card-header bg-secondary text-white">
<h5>💾 Información de Almacenamiento</h5>
</div>
<div class="card-body">
<p><strong>Datos básicos:</strong> MySQL</p>
<p><strong>Estadísticas:</strong> MongoDB</p>
<p><strong>ID MySQL:</strong> {{ libro.id }}</p>
<small class="text-muted">Sistema Híbrido Activo</small>
</div>
</div>
</div>
</div>
{% endblock %}
📄 TEMPLATE 5: crear_libro.html
Formulario para agregar un nuevo libro
<!-- ======================================== -->
<!-- ARCHIVO: libros/templates/libros/crear_libro.html -->
<!-- Formulario para crear nuevo libro -->
<!-- ======================================== -->
{% extends 'libros/base.html' %}
{% block title %}Agregar Nuevo Libro{% endblock %}
{% block content %}
<div class="mb-4">
<a href="{% url 'lista_libros' %}" class="btn btn-outline-secondary">← Volver al Catálogo</a>
</div>
<div class="card">
<div class="card-header" style="background: linear-gradient(90deg, #f093fb, #f5576c); color: white;">
<h2>➕ Agregar Nuevo Libro</h2>
</div>
<div class="card-body">
<form method="post" action="{% url 'crear_libro' %}">
{% csrf_token %}
<div class="row">
<div class="col-md-6 mb-3">
<label for="titulo" class="form-label">📝 Título *</label>
<input type="text" class="form-control" id="titulo" name="titulo" required>
</div>
<div class="col-md-6 mb-3">
<label for="autor" class="form-label">✍️ Autor *</label>
<input type="text" class="form-control" id="autor" name="autor" required>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="isbn" class="form-label">🔢 ISBN *</label>
<input type="text" class="form-control" id="isbn" name="isbn"
placeholder="978-3-16-148410-0" required>
</div>
<div class="col-md-4 mb-3">
<label for="fecha_publicacion" class="form-label">📅 Fecha Publicación *</label>
<input type="date" class="form-control" id="fecha_publicacion" name="fecha_publicacion" required>
</div>
<div class="col-md-4 mb-3">
<label for="precio" class="form-label">💰 Precio *</label>
<input type="number" step="0.01" class="form-control" id="precio" name="precio"
placeholder="0.00" required>
</div>
</div>
<div class="mb-3">
<label for="stock" class="form-label">📦 Stock</label>
<input type="number" class="form-control" id="stock" name="stock"
placeholder="0" value="1">
</div>
<div class="mb-3">
<label for="descripcion" class="form-label">📄 Descripción</label>
<textarea class="form-control" id="descripcion" name="descripcion"
rows="4" placeholder="Descripción opcional del libro..."></textarea>
</div>
<hr>
<div class="alert alert-info">
<strong>💡 Nota:</strong> Al crear el libro:
<ul>
<li>Se guardará en <strong>MySQL</strong> (datos estructurados)</li>
<li>Se creará documento en <strong>MongoDB</strong> (estadísticas iniciales)</li>
<li>Se registrará en <strong>logs de MongoDB</strong> (auditoría)</li>
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'lista_libros' %}" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary">💾 Guardar Libro</button>
</div>
</form>
</div>
</div>
{% endblock %}
📄 TEMPLATE 6: editar_libro.html
Formulario para modificar un libro existente
<!-- ======================================== -->
<!-- ARCHIVO: libros/templates/libros/editar_libro.html -->
<!-- Formulario para editar libro existente -->
<!-- ======================================== -->
{% extends 'libros/base.html' %}
{% block title %}Editar: {{ libro.titulo }}{% endblock %}
{% block content %}
<div class="mb-4">
<a href="{% url 'detalle_libro' libro.id %}" class="btn btn-outline-secondary">← Volver a Detalles</a>
</div>
<div class="card">
<div class="card-header" style="background: linear-gradient(90deg, #f093fb, #f5576c); color: white;">
<h2>✏️ Editar Libro: {{ libro.titulo }}</h2>
</div>
<div class="card-body">
<form method="post" action="{% url 'editar_libro' libro.id %}">
{% csrf_token %}
<div class="row">
<div class="col-md-6 mb-3">
<label for="titulo" class="form-label">📝 Título *</label>
<input type="text" class="form-control" id="titulo" name="titulo"
value="{{ libro.titulo }}" required>
</div>
<div class="col-md-6 mb-3">
<label for="autor" class="form-label">✍️ Autor *</label>
<input type="text" class="form-control" id="autor" name="autor"
value="{{ libro.autor }}" required>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="isbn" class="form-label">🔢 ISBN *</label>
<input type="text" class="form-control" id="isbn" name="isbn"
value="{{ libro.isbn }}" required>
</div>
<div class="col-md-4 mb-3">
<label for="fecha_publicacion" class="form-label">📅 Fecha Publicación *</label>
<input type="date" class="form-control" id="fecha_publicacion" name="fecha_publicacion"
value="{{ libro.fecha_publicacion|date:'Y-m-d' }}" required>
</div>
<div class="col-md-4 mb-3">
<label for="precio" class="form-label">💰 Precio *</label>
<input type="number" step="0.01" class="form-control" id="precio" name="precio"
value="{{ libro.precio }}" required>
</div>
</div>
<div class="mb-3">
<label for="stock" class="form-label">📦 Stock</label>
<input type="number" class="form-control" id="stock" name="stock"
value="{{ libro.stock }}">
</div>
<div class="mb-3">
<label for="descripcion" class="form-label">📄 Descripción</label>
<textarea class="form-control" id="descripcion" name="descripcion"
rows="4">{{ libro.descripcion }}</textarea>
</div>
<hr>
<div class="alert alert-warning">
<strong>⚠️ Importante:</strong> Al actualizar el libro:
<ul>
<li>Se modificará en <strong>MySQL</strong> (datos principales)</li>
<li>Se sincronizará con <strong>MongoDB</strong> (metadata)</li>
<li>Se mantendrán las estadísticas existentes</li>
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'detalle_libro' libro.id %}" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary">💾 Guardar Cambios</button>
</div>
</form>
</div>
</div>
{% endblock %}
📄 TEMPLATE 7: confirmar_eliminacion.html
Confirmación antes de eliminar un libro
<!-- ======================================== -->
<!-- ARCHIVO: libros/templates/libros/confirmar_eliminacion.html -->
<!-- Confirmación de eliminación de libro -->
<!-- ======================================== -->
{% extends 'libros/base.html' %}
{% block title %}Confirmar Eliminación{% endblock %}
{% block content %}
<div class="mb-4">
<a href="{% url 'detalle_libro' libro.id %}" class="btn btn-outline-secondary">← Volver a Detalles</a>
</div>
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h2>⚠️ Confirmar Eliminación</h2>
</div>
<div class="card-body">
<div class="alert alert-danger">
<h4>🗑️ ¿Estás seguro de eliminar este libro?</h4>
<p>Esta acción <strong>NO se puede deshacer</strong>.</p>
</div>
<h5>Información del Libro a Eliminar:</h5>
<table class="table table-bordered">
<tbody>
<tr>
<th width="200">Título:</th>
<td><strong>{{ libro.titulo }}</strong></td>
</tr>
<tr>
<th>Autor:</th>
<td>{{ libro.autor }}</td>
</tr>
<tr>
<th>ISBN:</th>
<td>{{ libro.isbn }}</td>
</tr>
<tr>
<th>Stock:</th>
<td>{{ libro.stock }} unidades</td>
</tr>
</tbody>
</table>
<form method="post" action="{% url 'eliminar_libro' libro.id %}">
{% csrf_token %}
<div class="mb-3">
<label for="motivo" class="form-label">Motivo de Eliminación (Opcional):</label>
<textarea class="form-control" id="motivo" name="motivo" rows="3"
placeholder="Ej: Libro descontinuado, duplicado, etc."></textarea>
</div>
<hr>
<div class="alert alert-info">
<strong>📝 Nota:</strong> Al eliminar el libro:
<ul>
<li>Se borrará de <strong>MySQL</strong> (datos principales)</li>
<li>Se archivará en <strong>MongoDB</strong> (para auditoría)</li>
<li>Las estadísticas se marcarán como "eliminado"</li>
<li>Se registrará en logs con motivo de eliminación</li>
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'detalle_libro' libro.id %}" class="btn btn-secondary">❌ Cancelar</a>
<button type="submit" class="btn btn-danger">🗑️ Sí, Eliminar Definitivamente</button>
</div>
</form>
</div>
</div>
{% endblock %}
📄 TEMPLATE 8: estadisticas.html
Dashboard con gráficas y métricas del sistema
<!-- ======================================== -->
<!-- ARCHIVO: libros/templates/libros/estadisticas.html -->
<!-- Dashboard de estadísticas con gráficas -->
<!-- ======================================== -->
{% extends 'libros/base.html' %}
{% block title %}Estadísticas del Sistema{% endblock %}
{% block content %}
<h1 class="mb-4">📊 Dashboard de Estadísticas</h1>
<!-- Resumen General -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h2 style="color: #f093fb;">{{ total_libros }}</h2>
<p class="text-muted">Total Libros (MySQL)</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h2 style="color: #f5576c;">{{ total_stock }}</h2>
<p class="text-muted">Stock Total</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h2 style="color: #667eea;">{{ total_views }}</h2>
<p class="text-muted">Vistas Totales (MongoDB)</p>
</div>
</div>
</div>
</div>
<!-- Gráficas -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-info text-white">
<h5>👁️ Top 10 Más Vistos</h5>
</div>
<div class="card-body">
<canvas id="chartVistos"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-success text-white">
<h5>📚 Top 10 Más Prestados</h5>
</div>
<div class="card-body">
<canvas id="chartPrestados"></canvas>
</div>
</div>
</div>
</div>
<!-- Actividad Reciente -->
<div class="card">
<div class="card-header bg-warning">
<h5>📝 Actividad Reciente (MongoDB Logs)</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Acción</th>
<th>Libro</th>
<th>Fecha y Hora</th>
</tr>
</thead>
<tbody>
{% for log in logs_recientes %}
<tr>
<td>
{% if log.accion == 'crear_libro' %}
<span class="badge bg-success">➕ Crear</span>
{% elif log.accion == 'editar_libro' %}
<span class="badge bg-primary">✏️ Editar</span>
{% elif log.accion == 'eliminar_libro' %}
<span class="badge bg-danger">🗑️ Eliminar</span>
{% elif log.accion == 'ver_detalle' %}
<span class="badge bg-info">👁️ Ver</span>
{% else %}
<span class="badge bg-secondary">{{ log.accion }}</span>
{% endif %}
</td>
<td>{{ log.libro_titulo|default:"N/A" }}</td>
<td><small>{{ log.timestamp|date:"d/m/Y H:i:s" }}</small></td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center text-muted">No hay actividad registrada</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}