📚 Guía Completa: Sistema de Biblioteca Híbrido
Django + MySQL + MongoDB

🎯 Objetivo del Proyecto

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.

📑 Tabla de Contenidos

1. Requisitos del Sistema

🖥️ Software Necesario

🐍 Python 3.8+

Versión recomendada: 3.13.7

Lenguaje de programación principal del proyecto.

Descargar Python

🗄️ MySQL 8.0+

Base de datos relacional para datos estructurados.

Opción A: XAMPP (incluye MySQL + phpMyAdmin)

Opción B: MySQL Server

🍃 MongoDB Atlas

Base de datos NoSQL en la nube (gratis hasta 512MB).

Crear cuenta en MongoDB Atlas

💻 Editor de Código

Recomendado: Visual Studio Code

Descargar VS Code

Otras opciones: PyCharm, Sublime Text

2. Tecnologías Utilizadas

Backend (Servidor)

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)

Frontend (Cliente)

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

3. Arquitectura del Sistema

🏗️ Patrón de Diseño: MVT (Model-View-Template)

Django utiliza el patrón MVT, una variante del MVC:

📊 Flujo de Datos

┌─────────────┐      ┌──────────────┐      ┌─────────────┐
│   Cliente   │ ───> │    Django    │ ───> │    MySQL    │
│ (Navegador) │      │   (Views)    │      │   (Libros,  │
└─────────────┘      └──────────────┘      │  Préstamos) │
                            │               └─────────────┘
                            │
                            ├──────────────> ┌──────────────┐
                            │                │   MongoDB    │
                            │                │   (Logs,     │
                            └──────────────> │ Estadísticas)│
                                             └──────────────┘
        

🗄️ Distribución de Datos

📦 MySQL - Datos Estructurados

🍃 MongoDB - Datos No Estructurados

4. Instalación y Configuración Inicial

1 Verificar Instalación de Python

Abre la terminal (CMD en Windows o Terminal en Mac/Linux) y ejecuta:

python --version

Deberías ver algo como Python 3.13.7. Si no está instalado, descárgalo de python.org.

2 Crear Carpeta del Proyecto

Crea una carpeta donde vivirá tu proyecto. Ejemplo:

mkdir C:\PRADODIAZ\biblioteca_project cd C:\PRADODIAZ\biblioteca_project

3 Crear Entorno Virtual

Un entorno virtual aísla las dependencias de tu proyecto:

python -m venv venv

💡 ¿Qué es un Entorno Virtual?

Es una carpeta que contiene una copia de Python y todas las librerías que instales. Esto evita conflictos entre proyectos diferentes.

4 Activar el Entorno Virtual

En Windows (CMD o PowerShell):
venv\Scripts\activate
En Mac/Linux:
source venv/bin/activate

Verás (venv) al inicio de tu línea de comandos, indicando que el entorno está activo.

5 Instalar Django y Dependencias

Crea un archivo llamado requirements.txt con el siguiente contenido:

📄 requirements.txt
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:

pip install -r requirements.txt

⚠️ Error Común: mysqlclient

Si mysqlclient falla al instalar:

  • Windows: Descarga el archivo .whl desde este enlace e instálalo con pip install nombre_archivo.whl
  • Mac: Instala con brew install mysql primero
  • Linux: Instala con sudo apt-get install python3-dev libmysqlclient-dev

6 Crear Proyecto Django

Ejecuta el siguiente comando para crear la estructura base de Django:

django-admin startproject biblioteca_project .

El punto . al final indica que se cree en la carpeta actual (no en una subcarpeta).

7 Crear Aplicación Django

Una aplicación Django es un módulo funcional dentro del proyecto:

python manage.py startapp biblioteca

Esto crea la carpeta biblioteca/ con archivos como models.py, views.py, etc.

5. Estructura del Proyecto

Después de completar los pasos anteriores, tu proyecto debería verse así:

biblioteca_project/ ├── venv/ # Entorno virtual (NO editar) ├── biblioteca_project/ # Configuración del proyecto │ ├── __init__.py │ ├── settings.py # ⚙️ Configuración principal │ ├── urls.py # 🔗 URLs principales │ ├── wsgi.py │ └── asgi.py ├── biblioteca/ # App principal │ ├── migrations/ # Migraciones de BD │ ├── templates/ # Plantillas HTML │ │ └── biblioteca/ │ │ ├── base.html # Template base │ │ ├── inicio.html │ │ ├── listar_libros.html │ │ ├── crear_libro.html │ │ ├── editar_libro.html │ │ ├── detalle_libro.html │ │ ├── confirmar_eliminacion.html │ │ ├── estadisticas.html │ │ ├── dashboard.html │ │ ├── mis_prestamos.html │ │ └── registrar_prestamo.html │ ├── __init__.py │ ├── admin.py # 🔧 Panel de administración │ ├── apps.py │ ├── models.py # 🗄️ Modelos de datos (MySQL) │ ├── views.py # 👁️ Lógica de vistas │ ├── urls.py # 🔗 URLs de la app │ ├── mongo_utils.py # 🍃 Utilidades MongoDB │ └── tests.py ├── manage.py # 🎮 Script de gestión Django ├── requirements.txt # 📦 Dependencias del proyecto └── README.md # 📖 Documentación

6. Configuración de MySQL

1 Instalar y Arrancar MySQL

Opción A: Usando XAMPP (Recomendado para principiantes)
  1. Descarga XAMPP desde apachefriends.org
  2. Instala y abre el "XAMPP Control Panel"
  3. Haz clic en "Start" junto a MySQL
  4. Haz clic en "Admin" para abrir phpMyAdmin en tu navegador
Opción B: MySQL Server Standalone
  1. Descarga MySQL desde MySQL Downloads
  2. Durante la instalación, configura un usuario root con contraseña (ej: 12345678)
  3. Inicia el servicio MySQL desde "Servicios" de Windows o con mysql.server start en Mac/Linux

2 Crear Base de Datos

Usando phpMyAdmin (XAMPP):
  1. Abre phpMyAdmin (http://localhost/phpmyadmin)
  2. Haz clic en "Nueva" en el panel izquierdo
  3. Escribe el nombre: bpd_biblioteca_db
  4. Cotejamiento: utf8mb4_general_ci
  5. Haz clic en "Crear"
Usando MySQL CLI:
mysql -u root -p CREATE DATABASE bpd_biblioteca_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; EXIT;

3 Configurar Django para MySQL

Abre el archivo biblioteca_project/settings.py y busca la sección DATABASES. Reemplázala con:

📄 biblioteca_project/settings.py
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'",
        }
    }
}

🔐 Seguridad

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')

4 Registrar la App en INSTALLED_APPS

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
]

7. Configuración de MongoDB Atlas

1 Crear Cuenta en MongoDB Atlas

  1. Ve a MongoDB Atlas
  2. Regístrate con tu correo (es gratis)
  3. Selecciona el plan M0 Free (512 MB, sin costo)
  4. Elige un proveedor de nube (AWS, Google Cloud o Azure) y región cercana a ti
  5. Haz clic en "Create Cluster" (tarda 3-5 minutos)

2 Configurar Acceso a la Base de Datos

A. Crear Usuario de Base de Datos
  1. En el panel izquierdo, haz clic en "Database Access"
  2. Haz clic en "Add New Database User"
  3. Usuario: biblioteca_admin
  4. Contraseña: Biblioteca2026! (o la que prefieras)
  5. Rol: "Read and write to any database"
  6. Haz clic en "Add User"
B. Configurar Whitelist de IPs
  1. En el panel izquierdo, haz clic en "Network Access"
  2. Haz clic en "Add IP Address"
  3. Haz clic en "Allow Access from Anywhere" (0.0.0.0/0)
  4. Haz clic en "Confirm"

⚠️ Advertencia de Seguridad

"Allow Access from Anywhere" es solo para desarrollo. En producción, especifica tu IP exacta.

3 Obtener Connection String

  1. En el panel izquierdo, haz clic en "Database"
  2. En tu cluster, haz clic en "Connect"
  3. Selecciona "Connect your application"
  4. Driver: Python, Versión: 3.12 or later
  5. Copia el connection string. Se verá así:
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

4 Crear Bases de Datos en MongoDB Atlas

  1. En el panel de tu cluster, haz clic en "Browse Collections"
  2. Haz clic en "Add My Own Data"
  3. Crea las siguientes bases de datos y colecciones:
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

5 Configurar Permisos en MongoDB

Necesitas configurar permisos para que el usuario biblioteca_admin pueda leer y escribir en todas las colecciones:

  1. Ve a "Database Access"
  2. Edita el usuario biblioteca_admin
  3. En "Database User Privileges", asegúrate de tener:
Base de Datos Permisos
biblioteca_logs Read and Write
biblioteca_catalogo Read and Write
biblioteca_estadisticas Read and Write
biblioteca_usuarios Read and Write

6 Agregar MongoDB a settings.py

Agrega la configuración de MongoDB al final de settings.py:

📄 biblioteca_project/settings.py (agregar al final)
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'],
}

🚨 IMPORTANTE: Reemplaza el Connection String

El connection string de arriba es un ejemplo. Debes usar el tuyo que copiaste de MongoDB Atlas.

7 Crear Archivo mongo_utils.py

Crea un archivo biblioteca/mongo_utils.py para gestionar conexiones a MongoDB:

📄 biblioteca/mongo_utils.py
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

8. Modelos de Django (MySQL)

📚 ¿Qué es un Modelo?

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:

📄 biblioteca/models.py
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}"

8 Crear Migraciones

Las migraciones son archivos que Django usa para crear/modificar tablas en MySQL:

python manage.py makemigrations

Deberías ver un mensaje como:

Migrations for 'biblioteca':
  biblioteca\migrations\0001_initial.py
    - Create model Libro
    - Create model Prestamo
    - Create model Multa

9 Aplicar Migraciones

Ejecuta las migraciones para crear las tablas en MySQL:

python manage.py migrate

Esto creará las tablas biblioteca_libro, biblioteca_prestamo y biblioteca_multa en MySQL.

✅ Verificación

Abre phpMyAdmin y verifica que existan las tablas en bpd_biblioteca_db:

9. Vistas (Views) del Sistema

👁️ ¿Qué es una Vista?

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:

📄 biblioteca/views.py (PARTE 1/3)
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)
📄 biblioteca/views.py (PARTE 2/3)
# ========================================
# 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)
📄 biblioteca/views.py (PARTE 3/3)
# ========================================
# 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)

✅ Completado

Has creado todas las vistas del sistema. Estas manejan:

10. Configuración de URLs

🔗 ¿Qué son las URLs en Django?

Las URLs son las rutas que escribes en el navegador (ej: /libros/, /libro/1/). Django las mapea a vistas específicas.

1 Crear archivo urls.py en la app

Crea el archivo biblioteca/urls.py:

📄 biblioteca/urls.py
from django.urls import path
from . import views

urlpatterns = [
    # Dashboard e Inicio
    path('', views.dashboard, name='dashboard'),
    path('inicio/', views.inicio, name='inicio'),
    
    # Listado y búsqueda de libros
    path('libros/', views.listar_libros, name='listar_libros'),
    
    # CRUD de libros
    path('libro/crear/', views.crear_libro, name='crear_libro'),
    path('libro//', views.detalle_libro, name='detalle_libro'),
    path('libro//editar/', views.editar_libro, name='editar_libro'),
    path('libro//eliminar/', views.eliminar_libro, name='eliminar_libro'),
    
    # Préstamos
    path('prestamo/crear//', views.prestar_libro, name='prestar_libro'),
    path('prestamo/devolver//', views.devolver_libro, name='devolver_libro'),
    path('prestamos/mis-prestamos/', views.mis_prestamos, name='mis_prestamos'),
    
    # Estadísticas
    path('estadisticas/', views.estadisticas, name='estadisticas'),
]

2 Conectar URLs al proyecto principal

Edita biblioteca_project/urls.py:

📄 biblioteca_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('biblioteca.urls')),  # ← Incluir URLs de la app
]
← Volver a Introducción Continuar a PARTE 2 - Templates →

📚 Has completado la PARTE 1 de la guía.
Continúa con la PARTE 2 para ver:
✅ Todos los templates HTML completos
✅ Configuración del panel de administración
✅ Pruebas y ejecución del sistema
✅ Solución de problemas comunes

🎓 Universidad Tecnológica de Hermosillo

Materia: Servicios Web

Proyecto: Sistema de Biblioteca Híbrido Django + MySQL + MongoDB

© 2026 - Guía Didáctica Completa