Construir un Sistema de Gestión de Biblioteca completamente funcional desde cero utilizando un enfoque de arquitectura híbrida:
👨🎓 Audiencia: Estudiantes sin conocimientos previos de programación que desean aprender desarrollo web full-stack.
Versión recomendada: 3.13.7
Lenguaje de programación principal del proyecto.
Base de datos relacional para datos estructurados.
Opción A: XAMPP (incluye MySQL + phpMyAdmin)
Opción B: MySQL Server
Recomendado: Visual Studio Code
Otras opciones: PyCharm, Sublime Text
| Tecnología | Versión | Propósito |
|---|---|---|
| Django | 4.1.13 | Framework web principal (MVC/MVT) |
| mysqlclient | 2.2.7 | Driver para conectar Django con MySQL |
| pymongo | 4.6.0 | Driver oficial para MongoDB |
| djongo | 1.3.6 | ORM de Django adaptado para MongoDB |
| djangorestframework | 3.14.0 | API REST (opcional, para futuras extensiones) |
| Tecnología | Versión | Propósito |
|---|---|---|
| Bootstrap | 5.3.0 | Framework CSS para diseño responsive |
| Chart.js | 4.x | Librería para gráficas interactivas |
| Font Awesome | 6.0 | Iconos vectoriales |
Django utiliza el patrón MVT, una variante del MVC:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Cliente │ ───> │ Django │ ───> │ MySQL │
│ (Navegador) │ │ (Views) │ │ (Libros, │
└─────────────┘ └──────────────┘ │ Préstamos) │
│ └─────────────┘
│
├──────────────> ┌──────────────┐
│ │ MongoDB │
│ │ (Logs, │
└──────────────> │ Estadísticas)│
└──────────────┘
Abre la terminal (CMD en Windows o Terminal en Mac/Linux) y ejecuta:
Deberías ver algo como Python 3.13.7. Si no está instalado, descárgalo de python.org.
Crea una carpeta donde vivirá tu proyecto. Ejemplo:
Un entorno virtual aísla las dependencias de tu proyecto:
Es una carpeta que contiene una copia de Python y todas las librerías que instales. Esto evita conflictos entre proyectos diferentes.
Verás (venv) al inicio de tu línea de comandos, indicando que el entorno está activo.
Crea un archivo llamado requirements.txt con el siguiente contenido:
asgiref==3.11.0
Django==4.1.13
djangorestframework==3.14.0
djongo==1.3.6
dnspython==2.4.2
mysqlclient==2.2.7
pymongo==4.6.0
pytz==2025.2
sqlparse==0.2.4
tzdata==2025.3
Luego instala todas las dependencias con:
Si mysqlclient falla al instalar:
.whl desde este enlace e instálalo con pip install nombre_archivo.whlbrew install mysql primerosudo apt-get install python3-dev libmysqlclient-devEjecuta el siguiente comando para crear la estructura base de Django:
El punto . al final indica que se cree en la carpeta actual (no en una subcarpeta).
Una aplicación Django es un módulo funcional dentro del proyecto:
Esto crea la carpeta biblioteca/ con archivos como models.py, views.py, etc.
Después de completar los pasos anteriores, tu proyecto debería verse así:
root con contraseña (ej: 12345678)mysql.server start en Mac/Linuxbpd_biblioteca_dbutf8mb4_general_ciAbre el archivo biblioteca_project/settings.py y busca la sección DATABASES. Reemplázala con:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'bpd_biblioteca_db', # Nombre de tu BD
'USER': 'root', # Usuario MySQL
'PASSWORD': '12345678', # Tu contraseña MySQL
'HOST': 'localhost', # Servidor local
'PORT': '3306', # Puerto por defecto
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
}
}
}
Nunca compartas tu settings.py con contraseñas reales. En producción, usa variables de entorno:
import os
'PASSWORD': os.environ.get('DB_PASSWORD', 'default_password')
En el mismo archivo settings.py, busca INSTALLED_APPS y agrega 'biblioteca':
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'biblioteca', # ← Agregar esta línea
]
biblioteca_adminBiblioteca2026! (o la que prefieras)"Allow Access from Anywhere" es solo para desarrollo. En producción, especifica tu IP exacta.
mongodb+srv://biblioteca_admin:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority
Reemplaza <password> con tu contraseña real (ej: Biblioteca2026!):
mongodb+srv://biblioteca_admin:Biblioteca2026!@cluster0.ixh8hty.mongodb.net/?retryWrites=true&w=majority
| Base de Datos | Colección Inicial | Propósito |
|---|---|---|
biblioteca_logs |
logs |
Auditoría de acciones del sistema |
biblioteca_catalogo |
libros |
Catálogo duplicado con metadata |
biblioteca_estadisticas |
estadisticas |
Métricas de uso (vistas, préstamos) |
biblioteca_usuarios |
usuarios |
Información extendida de usuarios |
Necesitas configurar permisos para que el usuario biblioteca_admin pueda leer y escribir en todas las colecciones:
biblioteca_admin| Base de Datos | Permisos |
|---|---|
biblioteca_logs |
Read and Write |
biblioteca_catalogo |
Read and Write |
biblioteca_estadisticas |
Read and Write |
biblioteca_usuarios |
Read and Write |
Agrega la configuración de MongoDB al final de settings.py:
from pymongo import MongoClient
# ========================================
# CONFIGURACIÓN MONGODB ATLAS
# ========================================
# Connection String de MongoDB Atlas
MONGODB_URI = "mongodb+srv://biblioteca_admin:Biblioteca2026!@cluster0.ixh8hty.mongodb.net/?retryWrites=true&w=majority"
MONGODB_CONNECTION_STRING = MONGODB_URI
MONGODB_DATABASE_NAME = "biblioteca_logs"
# Crear cliente MongoDB global
MONGO_CLIENT = MongoClient(MONGODB_URI)
# Diccionario de bases de datos de MongoDB
MONGODB_DATABASES = {
'logs': MONGO_CLIENT['biblioteca_logs'],
'catalogo': MONGO_CLIENT['biblioteca_catalogo'],
'estadisticas': MONGO_CLIENT['biblioteca_estadisticas'],
'usuarios': MONGO_CLIENT['biblioteca_usuarios'],
}
El connection string de arriba es un ejemplo. Debes usar el tuyo que copiaste de MongoDB Atlas.
Crea un archivo biblioteca/mongo_utils.py para gestionar conexiones 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
Un modelo en Django es una clase Python que representa una tabla en la base de datos MySQL. Cada atributo de la clase es una columna de la tabla.
Abre el archivo biblioteca/models.py y reemplaza todo su contenido con:
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
# ========================================
# MODELO: Libro
# ========================================
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)
# ========================================
# MODELO: Préstamo
# ========================================
class Prestamo(models.Model):
"""
Modelo para préstamos (se guarda en MySQL)
"""
# Relación con el libro
libro = models.ForeignKey(
'Libro',
on_delete=models.CASCADE,
verbose_name="Libro"
)
# Relación con usuario de Django
usuario = models.ForeignKey(
User,
on_delete=models.CASCADE,
verbose_name="Usuario"
)
# Fechas del préstamo
fecha_prestamo = models.DateTimeField(
auto_now_add=True,
verbose_name="Fecha de Préstamo"
)
fecha_devolucion = models.DateTimeField(
null=True,
blank=True,
verbose_name="Fecha de Devolución"
)
# Estado del préstamo
ESTADOS = [
('ACTIVO', 'Activo'),
('DEVUELTO', 'Devuelto'),
('VENCIDO', 'Vencido'),
]
estado = models.CharField(
max_length=10,
choices=ESTADOS,
default='ACTIVO',
verbose_name="Estado"
)
# Días de préstamo (se calculará desde MongoDB)
dias_prestamo = models.IntegerField(
default=14,
verbose_name="Días de Préstamo"
)
class Meta:
db_table = 'biblioteca_prestamo'
verbose_name = 'Préstamo'
verbose_name_plural = 'Préstamos'
ordering = ['-fecha_prestamo']
def __str__(self):
return f"Préstamo #{self.id} - {self.libro.titulo}"
# ========================================
# MODELO: Multa
# ========================================
class Multa(models.Model):
"""Modelo para multas por retraso en devolución"""
prestamo = models.ForeignKey(
Prestamo,
on_delete=models.CASCADE,
verbose_name="Préstamo"
)
monto = models.DecimalField(
max_digits=6,
decimal_places=2,
verbose_name="Monto"
)
pagada = models.BooleanField(
default=False,
verbose_name="Pagada"
)
fecha_creacion = models.DateTimeField(
auto_now_add=True,
verbose_name="Fecha de Creación"
)
class Meta:
db_table = 'biblioteca_multa'
verbose_name = 'Multa'
verbose_name_plural = 'Multas'
def __str__(self):
return f"Multa ${self.monto} - Préstamo #{self.prestamo.id}"
Las migraciones son archivos que Django usa para crear/modificar tablas en MySQL:
Deberías ver un mensaje como:
Migrations for 'biblioteca':
biblioteca\migrations\0001_initial.py
- Create model Libro
- Create model Prestamo
- Create model Multa
Ejecuta las migraciones para crear las tablas en MySQL:
Esto creará las tablas biblioteca_libro, biblioteca_prestamo y biblioteca_multa en MySQL.
Abre phpMyAdmin y verifica que existan las tablas en bpd_biblioteca_db:
biblioteca_librobiblioteca_prestamobiblioteca_multaauth_user (creada automáticamente por Django)Una vista en Django es una función Python que recibe una petición HTTP y devuelve una respuesta (HTML, JSON, etc.). Es el "controlador" del patrón MVT.
Abre biblioteca/views.py y reemplaza su contenido con:
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.conf import settings
from django.utils import timezone
from .models import Libro, Prestamo, Multa
from datetime import datetime, timedelta
from django.http import JsonResponse
from .mongo_utils import get_mongo_db
from bson import ObjectId
import json
# Obtener conexión a MongoDB
db = get_mongo_db()
# Función helper para registrar logs en MongoDB (opcional)
def log_to_mongodb(collection_name, log_data):
"""
Intenta registrar un log en MongoDB.
Si falla, continúa silenciosamente sin interrumpir la aplicación.
"""
try:
db[collection_name].insert_one(log_data)
except Exception:
pass # Ignorar errores de MongoDB
# ========================================
# VISTA: Página de Inicio
# ========================================
def inicio(request):
"""
Página principal del sistema con resumen de estadísticas.
Combina datos de MySQL (libros) y MongoDB (vistas/préstamos).
"""
# Estadísticas de MySQL
total_libros_mysql = Libro.objects.filter(activo=True).count()
libros_disponibles = Libro.objects.filter(activo=True, stock__gt=0).count()
# Estadísticas de MongoDB
total_libros_mongo = 0
top_libros = []
mongodb_conectado = False
try:
from pymongo import MongoClient
client = MongoClient(settings.MONGODB_CONNECTION_STRING)
db_stats = client['biblioteca_estadisticas']
# Contar documentos en MongoDB
total_libros_mongo = db_stats.estadisticas.count_documents({})
# Top 5 libros más vistos
top_libros = list(db_stats.estadisticas.find().sort('views', -1).limit(5))
mongodb_conectado = True
client.close()
except:
pass
context = {
'total_libros_mysql': total_libros_mysql,
'libros_disponibles': libros_disponibles,
'total_libros_mongo': total_libros_mongo,
'top_libros': top_libros,
'mongodb_conectado': mongodb_conectado
}
return render(request, 'biblioteca/inicio.html', context)
# ========================================
# VISTA: Listar Libros
# ========================================
def listar_libros(request):
"""
Lista todos los libros desde MySQL con búsqueda opcional.
"""
# Buscar parámetro de búsqueda
query = request.GET.get('q', '')
if query:
# Búsqueda en MySQL
libros = Libro.objects.filter(
titulo__icontains=query
) | Libro.objects.filter(
autor__icontains=query
)
else:
# Obtener todos los libros
libros = Libro.objects.all()
context = {
'libros': libros,
'query': query,
'total_libros': libros.count()
}
return render(request, 'biblioteca/listar_libros.html', context)
# ========================================
# VISTA: Detalle de Libro
# ========================================
def detalle_libro(request, libro_id):
"""
Muestra detalle de un libro desde MySQL.
Registra la vista en MongoDB.
"""
# Obtener libro desde MySQL
libro = get_object_or_404(Libro, id=libro_id)
# Obtener préstamos activos de este libro desde MySQL
prestamos_activos = Prestamo.objects.filter(
libro=libro,
estado='ACTIVO'
).select_related('usuario')
# Obtener estadísticas de MongoDB
views = 0
prestamos = 0
try:
client = settings.MONGO_CLIENT
db_stats = client['biblioteca_estadisticas']
# Buscar estadísticas del libro
stats = db_stats.estadisticas.find_one({'libro_id': libro_id})
if stats:
views = stats.get('views', 0)
prestamos = stats.get('prestamos', 0)
# Incrementar vistas en MongoDB
db_stats.estadisticas.update_one(
{'libro_id': libro_id},
{
'$inc': {'views': 1},
'$set': {
'titulo': libro.titulo,
'autor': libro.autor,
'ultima_vista': datetime.now()
}
},
upsert=True
)
views += 1 # Actualizar contador local
except Exception as e:
# Si falla MongoDB, continuar sin estadísticas
pass
# Registrar en logs
log_to_mongodb('logs', {
'accion': 'ver_detalle',
'libro_id': libro_id,
'libro_titulo': libro.titulo,
'timestamp': datetime.now()
})
context = {
'libro': libro,
'prestamos_activos': prestamos_activos,
'disponible': libro.disponible,
'views': views,
'prestamos': prestamos
}
return render(request, 'biblioteca/detalle_libro.html', context)
# ========================================
# VISTA: Crear Libro
# ========================================
def crear_libro(request):
"""
Formulario para agregar un nuevo libro.
Guarda en MySQL y registra 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:
# Verificar si el ISBN ya existe
if Libro.objects.filter(isbn=isbn).exists():
messages.error(request, f'❌ Ya existe un libro con el ISBN "{isbn}". Por favor usa un ISBN diferente.')
return render(request, 'biblioteca/crear_libro.html', {
'titulo': titulo,
'autor': autor,
'isbn': isbn,
'fecha_publicacion': fecha_publicacion,
'precio': precio,
'stock': stock,
'descripcion': descripcion
})
# 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
)
# Registrar en MongoDB (opcional)
try:
client = settings.MONGO_CLIENT
# Crear documento en catálogo
db_catalogo = client['biblioteca_catalogo']
db_catalogo.libros.insert_one({
'libro_id': libro.id,
'titulo': titulo,
'autor': autor,
'isbn': isbn,
'fecha_creacion': datetime.now()
})
# Crear estadísticas iniciales
db_stats = client['biblioteca_estadisticas']
db_stats.estadisticas.insert_one({
'libro_id': libro.id,
'titulo': titulo,
'autor': autor,
'views': 0,
'prestamos': 0,
'calificacion_promedio': 0.0,
'fecha_creacion': datetime.now()
})
except:
pass
# Registrar en logs
log_to_mongodb('logs', {
'accion': 'crear_libro',
'libro_id': libro.id,
'libro_titulo': titulo,
'isbn': isbn,
'timestamp': datetime.now()
})
messages.success(request, f'✅ Libro "{titulo}" creado exitosamente con ISBN {isbn}')
return redirect('listar_libros')
except Exception as e:
# Capturar error específico de ISBN duplicado
if '1062' in str(e) or 'Duplicate entry' in str(e):
messages.error(request, f'❌ El ISBN "{isbn}" ya está registrado. Usa un ISBN único.')
else:
messages.error(request, f'Error al crear libro: {str(e)}')
return render(request, 'biblioteca/crear_libro.html')
# ========================================
# VISTA: Editar Libro
# ========================================
def editar_libro(request, libro_id):
"""
Formulario para modificar un libro existente.
Actualiza MySQL y sincroniza con MongoDB.
"""
libro = get_object_or_404(Libro, id=libro_id)
if request.method == 'POST':
nuevo_isbn = request.POST.get('isbn')
try:
# Verificar si el nuevo ISBN ya existe en otro libro
if nuevo_isbn != libro.isbn:
if Libro.objects.filter(isbn=nuevo_isbn).exclude(id=libro_id).exists():
messages.error(request, f'❌ Ya existe otro libro con el ISBN "{nuevo_isbn}".')
return render(request, 'biblioteca/editar_libro.html', {'libro': libro})
# Actualizar campos
libro.titulo = request.POST.get('titulo')
libro.autor = request.POST.get('autor')
libro.isbn = nuevo_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', '')
# Guardar en MySQL
libro.save()
# Sincronizar con MongoDB (opcional)
try:
client = settings.MONGO_CLIENT
db_stats = client['biblioteca_estadisticas']
db_stats.estadisticas.update_one(
{'libro_id': libro_id},
{
'$set': {
'titulo': libro.titulo,
'autor': libro.autor,
'ultima_actualizacion': datetime.now()
}
}
)
except:
pass
# Registrar en logs
log_to_mongodb('logs', {
'accion': 'editar_libro',
'libro_id': libro_id,
'libro_titulo': libro.titulo,
'isbn': libro.isbn,
'timestamp': datetime.now()
})
messages.success(request, f'✅ Libro "{libro.titulo}" actualizado exitosamente')
return redirect('detalle_libro', libro_id=libro_id)
except Exception as e:
if '1062' in str(e) or 'Duplicate entry' in str(e):
messages.error(request, f'❌ El ISBN "{nuevo_isbn}" ya está registrado en otro libro.')
else:
messages.error(request, f'Error al actualizar: {str(e)}')
context = {'libro': libro}
return render(request, 'biblioteca/editar_libro.html', context)
# ========================================
# VISTA: Eliminar Libro
# ========================================
def eliminar_libro(request, libro_id):
"""
Elimina un libro del sistema MySQL.
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
try:
client = settings.MONGO_CLIENT
db_logs = client['biblioteca_logs']
db_logs.libros_eliminados.insert_one({
'libro_id': libro_id,
'titulo': titulo,
'autor': libro.autor,
'isbn': libro.isbn,
'fecha_eliminacion': datetime.now(),
'motivo': request.POST.get('motivo', 'No especificado')
})
except:
pass
# Eliminar de MySQL
libro.delete()
# Registrar en logs
log_to_mongodb('logs', {
'accion': 'eliminar_libro',
'libro_id': libro_id,
'libro_titulo': titulo,
'timestamp': datetime.now()
})
messages.success(request, f'Libro "{titulo}" eliminado correctamente')
return redirect('listar_libros')
except Exception as e:
messages.error(request, f'Error al eliminar: {str(e)}')
context = {'libro': libro}
return render(request, 'biblioteca/confirmar_eliminacion.html', context)
# ========================================
# VISTA: Estadísticas
# ========================================
def estadisticas(request):
"""
Dashboard con métricas y gráficas desde 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
libros_activos = Libro.objects.filter(activo=True).count()
libros_disponibles = Libro.objects.filter(activo=True, stock__gt=0).count()
# Estadísticas de MongoDB
total_views = 0
top_vistos = []
top_prestados = []
try:
from pymongo import MongoClient
client = MongoClient(settings.MONGODB_CONNECTION_STRING)
db_stats = client['biblioteca_estadisticas']
# Total de vistas (suma de todos los views)
pipeline = [
{'$group': {'_id': None, 'total_views': {'$sum': '$views'}}}
]
result = list(db_stats.estadisticas.aggregate(pipeline))
total_views = result[0]['total_views'] if result else 0
# Top 10 más vistos
top_vistos = list(db_stats.estadisticas.find().sort('views', -1).limit(10))
# Top 10 más prestados
top_prestados = list(db_stats.estadisticas.find().sort('prestamos', -1).limit(10))
client.close()
except Exception as e:
# Si falla MongoDB, continuar con valores vacíos
pass
# Actividad reciente (últimos 20 logs de MongoDB)
try:
logs_recientes = list(db.logs.find().sort('timestamp', -1).limit(20))
except:
logs_recientes = []
# Registrar acceso al dashboard
log_to_mongodb('logs', {
'accion': 'ver_estadisticas',
'timestamp': datetime.now()
})
context = {
'total_libros': total_libros,
'total_stock': total_stock,
'libros_activos': libros_activos,
'libros_disponibles': libros_disponibles,
'total_views': total_views,
'top_vistos': top_vistos,
'top_prestados': top_prestados,
'logs_recientes': logs_recientes
}
return render(request, 'biblioteca/estadisticas.html', context)
# ========================================
# VISTA: Dashboard
# ========================================
def dashboard(request):
"""
Panel de control con métricas del sistema.
Combina datos de MySQL y MongoDB.
"""
# Estadísticas de MySQL
total_prestamos_activos = Prestamo.objects.filter(estado='ACTIVO').count()
total_prestamos_historico = Prestamo.objects.count()
total_libros = Libro.objects.count()
libros_disponibles = Libro.objects.filter(activo=True, stock__gt=0).count()
# Estadísticas de MongoDB
mongodb_stats = {
'logs_count': 0,
'libros_count': 0,
'estadisticas_count': 0,
'usuarios_count': 0,
'total_documentos': 0,
'mongodb_conectado': False
}
try:
# Contar documentos en cada colección de MongoDB
mongodb_stats['logs_count'] = db.logs.count_documents({})
# Contar en otras bases de datos
client = settings.MONGO_CLIENT
mongodb_stats['libros_count'] = client['biblioteca_catalogo'].libros.count_documents({})
mongodb_stats['estadisticas_count'] = client['biblioteca_estadisticas'].estadisticas.count_documents({})
mongodb_stats['usuarios_count'] = client['biblioteca_usuarios'].usuarios.count_documents({})
# Total de documentos en MongoDB
mongodb_stats['total_documentos'] = (
mongodb_stats['logs_count'] +
mongodb_stats['libros_count'] +
mongodb_stats['estadisticas_count'] +
mongodb_stats['usuarios_count']
)
mongodb_stats['mongodb_conectado'] = True
except Exception as e:
# Si MongoDB no está disponible, mantener valores en 0
mongodb_stats['mongodb_conectado'] = False
# Registrar acceso al dashboard
log_to_mongodb('logs', {
'accion': 'acceso_dashboard',
'timestamp': datetime.now(),
'ip': request.META.get('REMOTE_ADDR', 'unknown')
})
context = {
'total_prestamos_activos': total_prestamos_activos,
'total_prestamos_historico': total_prestamos_historico,
'total_libros': total_libros,
'libros_disponibles': libros_disponibles,
'mongodb_stats': mongodb_stats,
}
return render(request, 'biblioteca/dashboard.html', context)
# ========================================
# VISTAS ADICIONALES: Préstamos
# ========================================
@login_required
def prestar_libro(request, libro_id):
"""
Crea un préstamo en MySQL.
"""
if request.method == 'POST':
libro = get_object_or_404(Libro, id=libro_id)
if libro.stock <= 0 or not libro.activo:
messages.error(request, f"El libro '{libro.titulo}' no tiene stock disponible")
return redirect('detalle_libro', libro_id=libro_id)
prestamo_existente = Prestamo.objects.filter(
libro=libro,
usuario=request.user,
estado='ACTIVO'
).exists()
if prestamo_existente:
messages.warning(request, "Ya tienes un préstamo activo de este libro")
return redirect('detalle_libro', libro_id=libro_id)
try:
prestamo = Prestamo.objects.create(
libro=libro,
usuario=request.user,
dias_prestamo=14
)
libro.stock -= 1
libro.save()
# Registrar en MongoDB
try:
client = settings.MONGO_CLIENT
db_stats = client['biblioteca_estadisticas']
db_stats.estadisticas.update_one(
{'libro_id': libro_id},
{'$inc': {'prestamos': 1}},
upsert=True
)
except:
pass
log_to_mongodb('logs', {
"timestamp": datetime.now(),
"usuario": request.user.username,
"accion": "PRESTAMO_CREADO",
"libro_id": libro.id,
"prestamo_id": prestamo.id,
"detalles": f"Préstamo del libro '{libro.titulo}'"
})
messages.success(
request,
f"✅ Préstamo registrado exitosamente. Fecha de devolución: "
f"{(timezone.now() + timedelta(days=14)).strftime('%d/%m/%Y')}"
)
return redirect('mis_prestamos')
except Exception as e:
messages.error(request, f"Error al crear préstamo: {str(e)}")
return redirect('detalle_libro', libro_id=libro_id)
return redirect('detalle_libro', libro_id=libro_id)
@login_required
def devolver_libro(request, prestamo_id):
"""
Marca préstamo como devuelto en MySQL.
"""
prestamo = get_object_or_404(Prestamo, id=prestamo_id, usuario=request.user)
if prestamo.estado != 'ACTIVO':
messages.warning(request, "Este préstamo ya fue devuelto")
return redirect('mis_prestamos')
try:
prestamo.estado = 'DEVUELTO'
prestamo.fecha_devolucion = timezone.now()
prestamo.save()
libro = prestamo.libro
libro.stock += 1
libro.save()
log_to_mongodb('logs', {
"timestamp": datetime.now(),
"usuario": request.user.username,
"accion": "DEVOLUCION",
"libro_id": prestamo.libro.id,
"prestamo_id": prestamo.id
})
messages.success(request, "✅ Libro devuelto exitosamente")
except Exception as e:
messages.error(request, f"Error al devolver libro: {str(e)}")
return redirect('mis_prestamos')
@login_required
def mis_prestamos(request):
"""
Lista préstamos del usuario desde MySQL.
"""
prestamos = Prestamo.objects.filter(
usuario=request.user
).select_related('libro').order_by('-fecha_prestamo')
context = {
'prestamos': prestamos,
'total_prestamos': prestamos.count()
}
return render(request, 'biblioteca/mis_prestamos.html', context)