📚 Guía Completa de Instalación Examen U3

Sistema de Gestión de Biblioteca con Django, MySQL, REST API y SOAP

✨ Guía paso a paso ✨

📑 Tabla de Contenidos

  1. Requisitos Previos y Verificación del Sistema
  2. Instalación de Python 3.11
  3. Instalación y Configuración de MySQL
  4. Instalación de Docker Desktop (OBLIGATORIO)
  5. Creación del Proyecto Django
  6. Estructura Completa del Proyecto
  7. Configuración de Base de Datos MySQL
  8. Creación de Modelos
  9. Migraciones de Base de Datos
  10. Configuración de API REST
  11. Configuración de Servicio SOAP
  12. Creación de Templates HTML
  13. Archivos Estáticos (CSS y JavaScript)
  14. Configuración del Panel Admin
  15. Población de Datos de Prueba
  16. Ejecución y Pruebas del Sistema
  17. Pruebas del Servicio SOAP (cliente_soap_visual.py)
  18. Pruebas de APIs REST y SOAP con Postman
  19. Despliegue con Docker (Producción)
  20. Solución de Problemas Comunes
  21. Código Completo de Archivos Auxiliares
  22. Funcionalidades Extras y Mejoras
  23. 🎬 Checklist de Verificación Final (Para Grabación)
  24. 🚀 Despliegue en Producción (GitHub + PythonAnywhere)

1. Requisitos Previos y Verificación del Sistema

ℹ️ Antes de comenzar: Esta guía está diseñada para que Cada paso sea explicado en detalle.

1.1. Requisitos de Hardware

Componente Mínimo Recomendado
Procesador Dual Core 2.0 GHz Quad Core 2.5 GHz o superior
RAM 4 GB 8 GB o más
Espacio en Disco 5 GB libres 10 GB libres
Sistema Operativo Windows 10/11, macOS 10.14+, Ubuntu 20.04+

1.2. Software Necesario

  • Python 3.11 (se instalará en el siguiente paso)
  • MySQL 8.0 o superior
  • Editor de texto (VS Code recomendado, Notepad++ o cualquier otro)
  • Navegador web moderno (Chrome, Firefox, Edge)
  • Terminal/CMD (viene incluido en Windows)

1.3. Verificar si Python está instalado

1 Abrir la terminal de comandos

En Windows:
• Presiona las teclas Windows + R
• Escribe cmd y presiona Enter
• Se abrirá una ventana negra (la terminal)

2 Verificar Python
# Ejecuta este comando: python --version

Resultados posibles:
• Si aparece "Python 3.11.X": ✅ Python está instalado correctamente
• Si aparece un error: ❌ Debes instalar Python (siguiente sección)

2. Instalación de Python 3.11

⚠️ Importante: Si ya tienes Python 3.11 instalado (verificado en el paso anterior), puedes saltar esta sección e ir directamente a la Sección 3.

2.1. Descargar Python

1 Ir al sitio oficial

1. Abre tu navegador web
2. Ve a: https://www.python.org/downloads/
3. Haz clic en el botón amarillo que dice "Download Python 3.11.X"

2 Instalar Python

1. Ejecuta el archivo descargado (python-3.11.X-amd64.exe)
2. IMPORTANTE: NO marques la casilla "Add Python 3.11 to PATH" (usaremos un entorno virtual)
3. Haz clic en "Install Now"
4. Espera a que termine la instalación (2-5 minutos)
5. Haz clic en "Close"

ℹ️ Nota: No agregamos Python al PATH porque este proyecto es solo para práctica. Usaremos un entorno virtual que mantiene el proyecto aislado sin afectar tu sistema.
3 Verificar instalación

1. Cierra cualquier terminal que tengas abierta
2. Abre una nueva terminal (Windows + R, luego "cmd")
3. Ejecuta:

python --version pip --version

Deberías ver algo como:
Python 3.11.9
pip 24.0 from ...

¡Python instalado correctamente! Puedes continuar con la siguiente sección.

3. Instalación y Configuración de MySQL

3.1. Descargar MySQL

1 Ir al sitio oficial de MySQL

1. Abre tu navegador
2. Ve a: https://dev.mysql.com/downloads/installer/
3. Descarga "MySQL Installer for Windows" (el archivo más grande, ~400 MB)

3.2. Instalar MySQL

2 Ejecutar el instalador

1. Ejecuta el archivo descargado (mysql-installer-community-X.X.X.msi)
2. Selecciona "Server only" (Solo servidor)
3. Haz clic en "Next" → "Execute" → Espera la instalación
4. En "Type and Networking": deja todo por defecto (Puerto 3306)
5. En "Authentication Method": selecciona "Use Strong Password Encryption"

3 Configurar contraseña de root

1. Cuando pida "MySQL Root Password", ingresa TU CONTRASEÑA
2. Repite la contraseña en "Repeat Password"
3. Haz clic en "Next" hasta completar la instalación

⚠️ MUY IMPORTANTE: Guarda la contraseña que elijas. La necesitarás para configurar Django más adelante.

Recuerda tu contraseña: _________________________

3.3. Crear la Base de Datos

4 Abrir MySQL Command Line Client

1. Presiona la tecla Windows
2. Busca "MySQL Command Line Client"
3. Ábrelo (se abrirá una ventana negra)
4. Te pedirá la contraseña: escribe TU CONTRASEÑA de MySQL y presiona Enter

5 Crear la base de datos

Ejecuta los siguientes comandos uno por uno:

# Crear la base de datos CREATE DATABASE biblioteca_dbutres CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
# Verificar que se creó SHOW DATABASES;
# Salir de MySQL exit;

Deberías ver "biblioteca_dbutres" en la lista de bases de datos.

¡MySQL configurado correctamente! Base de datos "biblioteca_dbutres" creada.

4. Instalación de Docker Desktop (OBLIGATORIO)

🎨 Aqui se puede ver esta sección con diseño profesional mejorado

✨ Abrir Documento ✨

(Se abrirá en una nueva pestaña)

⚠️ IMPORTANTE: Docker Desktop es OBLIGATORIO para este proyecto. El sistema está diseñado para ejecutarse en contenedores Docker, lo que garantiza un entorno consistente y profesional para todos los estudiantes.
ℹ️ ¿Qué es Docker? Docker es una plataforma que permite ejecutar aplicaciones en contenedores aislados. Es la forma estándar en la industria para desplegar aplicaciones de forma profesional y escalable.

4.1. ¿Por qué es obligatorio Docker para este proyecto?

Ventajas de Docker Desktop (Razones de uso obligatorio)
  • 🎯 Consistencia Total: Mismo entorno para todos los estudiantes
  • 🎯 Configuración Automática: MySQL, Django, Nginx y Redis pre-configurados
  • 🎯 Evita Conflictos: No interfiere con otras instalaciones en tu PC
  • 🎯 Despliegue Instantáneo: Todo listo con un solo comando
  • 🎯 Simula Producción Real: Arquitectura profesional de microservicios
  • 🎯 Balanceo de Carga: Nginx distribuye peticiones automáticamente
  • 🎯 Fácil Reset: Borra y recrea el ambiente en segundos
  • 🎯 Estándar Profesional: Tecnología usada en empresas reales
💡 Ventaja Principal: Con Docker, tu proyecto funcionará exactamente igual en tu computadora, en la de tu profesor y en cualquier servidor de producción. No más problemas de "en mi máquina funciona".

4.2. Requisitos previos para Docker

Verificar requisitos del sistema
Requisito Mínimo ¿Cómo verificar?
Sistema Operativo Windows 10/11 (64-bit) Presiona Win + Pause
RAM 4 GB (8 GB recomendado) Presiona Win + Pause
Virtualización Habilitada en BIOS Abre "Administrador de Tareas" → Pestaña "Rendimiento" → CPU → Debe decir "Virtualización: Habilitada"
WSL 2 Necesario para Windows Se instalará en el siguiente paso
⚠️ Si la virtualización está deshabilitada:
1. Reinicia tu PC
2. Entra a la BIOS (presiona F2, F10, F12 o DEL al iniciar, depende de tu marca de PC)
3. Busca "Virtualization Technology", "VT-x", "AMD-V" o "SVM" y habilítala
4. Guarda cambios (usualmente F10) y reinicia

4.3. Descargar Docker Desktop

1 Ir al sitio oficial de Docker

Opción A - Descarga desde el navegador:
1. Abre tu navegador web (Chrome, Firefox, Edge, etc.)
2. Ve a: https://www.docker.com/products/docker-desktop/
3. Haz clic en el botón azul grande "Download for Windows"
4. Se descargará el archivo "Docker Desktop Installer.exe" (~500 MB)
5. Espera a que termine la descarga (5-15 minutos según tu velocidad de internet)

💡 Opción B - Si ya tienes el instalador:
Si ya tienes el archivo "Docker Desktop Installer.exe" en tu carpeta del proyecto o en Descargas, puedes usarlo directamente (pasa al siguiente paso).

4.4. Instalar Docker Desktop

1 Ejecutar el instalador

Paso a paso detallado:

  1. Haz doble clic en el archivo "Docker Desktop Installer.exe"
  2. Si aparece la ventana de "Control de Cuentas de Usuario", haz clic en "Sí"
  3. Aparecerá la ventana de configuración de Docker Desktop
📸 Ventana: "Docker Desktop Installer - Configuration"
2 Configurar opciones de instalación

⚠️ IMPORTANTE - Marca estas opciones:

☑️ Use WSL 2 instead of Hyper-V (recomendado)
→ Esta opción DEBE estar marcada (por defecto lo está)

☑️ Add shortcut to desktop
→ Crea un acceso directo en el escritorio (opcional pero útil)

3. Haz clic en el botón "Ok" para iniciar la instalación

3 Esperar la instalación

Durante la instalación verás:

  • "Unpacking files..." (Descomprimiendo archivos) → 1-2 minutos
  • "Installing..." (Instalando) → 3-5 minutos
  • "Configuring..." (Configurando) → 1-2 minutos

Tiempo total: 5-10 minutos dependiendo de la velocidad de tu PC

Buen momento para: Tomar un café, estirar las piernas, revisar tus apuntes o ver un video corto. La instalación continuará automáticamente.
4 Reiniciar la computadora (OBLIGATORIO)

Cuando termine la instalación, aparecerá un mensaje similar a este:

✅ "Installation succeeded"
⚠️ "Docker Desktop requires a reboot to complete the installation"

⚠️ ACCIÓN REQUERIDA - Sigue estos pasos:
1. Guarda TODO tu trabajo (documentos abiertos, pestañas del navegador, etc.)
2. Cierra todas las aplicaciones que tengas abiertas
3. Haz clic en el botón "Close and restart"
4. Tu PC se reiniciará automáticamente en unos segundos
5. Espera a que Windows vuelva a iniciar completamente (1-3 minutos)

4.5. Configuración inicial de Docker Desktop

1 Primer inicio después del reinicio

Cuando Windows vuelva a iniciar:

  1. Busca el ícono de Docker en el escritorio (ballena azul con contenedores) 🐋
  2. Haz doble clic para abrir Docker Desktop
    Si no está en el escritorio, presiona Windows y busca "Docker Desktop"
  3. Espera unos 10-20 segundos mientras Docker inicia por primera vez
  4. Aparecerá la ventana de bienvenida de Docker Desktop
2 Aceptar términos y condiciones

1. Aparecerá una ventana con los términos de servicio de Docker
2. Lee los términos (o no, seamos honestos 😄)
3. Marca la casilla ☑️ "I accept the terms"
4. Haz clic en el botón "Accept"

3 Configurar cuenta Docker Hub (opcional - recomendado omitir)

Aparecerá una ventana preguntando si quieres iniciar sesión o crear una cuenta de Docker Hub:

Opción A - Sin cuenta (⭐ Recomendado para comenzar rápido):

  • Haz clic en "Skip" o "Continue without signing in"
  • Podrás usar Docker completamente sin cuenta
  • Puedes crear cuenta después si lo necesitas (no es necesario para este proyecto)

Opción B - Con cuenta (Opcional):

  • Haz clic en "Sign up" para crear una cuenta gratuita de Docker Hub
  • Ingresa: email, nombre de usuario, contraseña
  • Verifica tu email (recibirás un correo de confirmación)
  • Regresa a Docker Desktop e inicia sesión
💡 Nuestra recomendación: Por ahora, haz clic en "Skip". No necesitas una cuenta de Docker Hub para usar Docker localmente en tu proyecto.
4 Encuesta inicial de Docker (omitir)

Docker puede mostrar una breve encuesta preguntando:

  • "What will you be using Docker for?" (¿Para qué usarás Docker?)
  • "What is your role?" (¿Cuál es tu rol?)

Puedes:
• Responderla si quieres (selecciona "Learning" / "Student")
• O simplemente haz clic en "Skip" en la parte inferior

5 Esperar que Docker inicie completamente

Verás la interfaz principal de Docker Desktop con:

En la parte inferior izquierda de la ventana, verás el estado:

  • 🔴 "Starting..." (Iniciando) → Espera pacientemente 1-2 minutos
  • 🟡 "Starting Docker Desktop..." (Iniciando servicios) → Casi listo, unos segundos más
  • 🟢 "Docker Desktop is running" o "Engine running" → ¡Listo para usar!

También verás el ícono de Docker en la bandeja del sistema (esquina inferior derecha de Windows):

  • 🐋 Ballena animándose = Docker está iniciando
  • 🐋 Ballena estática = Docker corriendo correctamente ✅
¡Docker Desktop instalado y corriendo!
Cuando veas "Docker Desktop is running" en verde, Docker está listo para usar.
Puedes minimizar la ventana de Docker Desktop, seguirá corriendo en segundo plano.

4.6. Verificar instalación de Docker

1 Abrir una terminal de comandos

1. Presiona las teclas Windows + R
2. Escribe cmd
3. Presiona Enter
4. Se abrirá la ventana negra de comandos (CMD)

2 Verificar versión de Docker

Copia y pega este comando en la ventana negra:

# Verificar que Docker está instalado docker --version

Salida esperada (algo similar a):

Docker version 24.0.7, build afdd53b

Los números de versión pueden variar (puede ser 24.0.6, 25.0.0, etc.). Lo importante es que NO salga un mensaje de error.

⚠️ Si sale un error: Asegúrate de que Docker Desktop esté abierto y corriendo (debe decir "Engine running"). Si no, ábrelo desde el menú inicio.
3 Verificar Docker Compose

Docker Compose es necesario para orquestar múltiples contenedores:

# Verificar Docker Compose (lo usaremos para el proyecto) docker-compose --version

Salida esperada:

Docker Compose version v2.23.0

Si ves la versión, significa que Docker Compose está instalado correctamente (viene incluido con Docker Desktop).

4 Probar Docker con un contenedor de prueba

Este comando descarga y ejecuta un contenedor de prueba simple para verificar que todo funciona:

# Descargar y ejecutar contenedor de prueba "hello-world" docker run hello-world

¿Qué pasará? (paso a paso):

  1. Docker buscará la imagen "hello-world" en tu computadora (no la encontrará)
  2. La descargará automáticamente de Docker Hub (solo ~2 KB, toma 2-3 segundos)
  3. Verás: Unable to find image 'hello-world:latest' locally
  4. Luego: latest: Pulling from library/hello-world
  5. Ejecutará el contenedor automáticamente
  6. Mostrará un mensaje de éxito
  7. El contenedor se detendrá automáticamente (ya hizo su trabajo)

Salida esperada completa:

Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 719385e32844: Pull complete Digest: sha256:... Status: Downloaded newer image for hello-world:latest
Hello from Docker! This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. 3. The Docker daemon created a new container from that image... 4. The Docker daemon streamed that output to the Docker client...
5 Verificar que el contenedor se ejecutó correctamente

Para ver todos los contenedores que se han ejecutado (incluso los que ya terminaron):

# Ver todos los contenedores (activos e inactivos) docker ps -a

Deberías ver una tabla con el contenedor "hello-world" en estado Exited (0):

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES abc123def456 hello-world "/hello" 10 seconds ago Exited (0) 9 seconds ago eloquent_tesla

Explicación:
CONTAINER ID: Identificador único del contenedor
IMAGE: Imagen usada (hello-world)
STATUS: Exited (0) significa que terminó correctamente
NAMES: Nombre aleatorio asignado por Docker

¡Docker funciona perfectamente!
Si viste el mensaje "Hello from Docker!", tu instalación está completamente funcional.
Ahora puedes crear y ejecutar contenedores para tu proyecto.

4.7. Configuración adicional recomendada

1 Ajustar recursos de Docker (Recomendado si tienes 8+ GB RAM)

Si tu PC tiene 8 GB o más de RAM, puedes asignar más recursos a Docker para mejor rendimiento:

  1. Abre Docker Desktop (si lo cerraste)
  2. Haz clic en el ícono de engranaje ⚙️ (Settings) en la parte superior derecha
  3. Ve a la sección "Resources""Advanced" (en el menú izquierdo)
  4. Ajusta los siguientes valores:
    CPUs: 2-4 (dependiendo de cuántos núcleos tenga tu procesador)
    Memory: 4-6 GB (si tienes 8-16 GB RAM total)
    Swap: 1 GB
    Disk image size: 64 GB (el valor por defecto está bien)
  5. Haz clic en "Apply & restart" en la parte inferior derecha
  6. Espera unos segundos mientras Docker se reinicia
⚠️ No asignes toda tu RAM a Docker: Deja al menos 2-3 GB disponibles para Windows y otras aplicaciones. Si tienes 4 GB RAM total, deja los valores por defecto.
2 Habilitar integración con WSL (verificar)

Asegúrate de que Docker esté integrado correctamente con WSL 2:

  1. En Docker Desktop, haz clic en Settings ⚙️
  2. Ve a "Resources""WSL Integration"
  3. Verifica que esta opción esté activada:
    ☑️ "Enable integration with my default WSL distro"
  4. Si aparece "Ubuntu" en la lista de distribuciones, actívala también:
    ☑️ Ubuntu
  5. Haz clic en "Apply & restart"

4.8. ¿Qué hace nuestro sistema con Docker?

ℹ️ Arquitectura del proyecto con Docker:

Nuestro proyecto incluye archivos de configuración que permiten desplegar toda la infraestructura automáticamente:

Dockerfile: Define cómo construir la imagen de Django con todas sus dependencias
docker-compose.yml: Orquesta 4 servicios que trabajan juntos:
   1. 🗄️ MySQL - Base de datos relacional
   2. 🐍 Django Web - Aplicación principal (REST API + SOAP)
   3. 🌐 Nginx - Servidor web y balanceador de carga
   4. 🔴 Redis - Sistema de caché (opcional pero útil)

Con un solo comando docker-compose up se levanta todo el sistema completo y en 2-3 minutos está funcionando.
Diagrama de arquitectura
┌─────────────────────────────────────────────────────────┐
│                    NAVEGADOR WEB                        │
│            http://localhost or :8000                    │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
         ┌───────────────────────┐
         │   NGINX (Puerto 80)   │ ← Balanceador de carga
         │   Servidor web        │
         └───────────┬───────────┘
                     │
         ┌───────────┴───────────┐
         │                       │
         ▼                       ▼
┌─────────────────┐     ┌─────────────────┐
│  DJANGO WEB #1  │     │  DJANGO WEB #2  │ ← Escalable
│  (Puerto 8000)  │     │  (Puerto 8001)  │
│  REST + SOAP    │     │  REST + SOAP    │
└────────┬────────┘     └────────┬────────┘
         │                       │
         └───────────┬───────────┘
                     │
         ┌───────────┴───────────┐
         │                       │
         ▼                       ▼
┌─────────────────┐     ┌─────────────────┐
│  MySQL DB       │     │  Redis Cache    │
│  (Puerto 3306)  │     │  (Puerto 6379)  │
│  Datos          │     │  Sesiones       │
└─────────────────┘     └─────────────────┘

4.10. Comandos útiles de Docker (Referencia rápida)

📋 Guarda esta referencia: Estos comandos te serán muy útiles durante todo el proyecto.
Comando Descripción Cuándo usarlo
docker ps Ver contenedores en ejecución Verificar qué está corriendo
docker ps -a Ver todos los contenedores Ver historial completo
docker images Ver imágenes descargadas Ver qué imágenes tienes
docker-compose up Iniciar todos los servicios Levantar el proyecto completo
docker-compose up -d Iniciar en segundo plano No bloquear la terminal
docker-compose down Detener todos los servicios Apagar todo el sistema
docker-compose logs Ver logs de los servicios Depurar errores
docker-compose logs -f Ver logs en tiempo real Monitorear actividad
docker-compose restart Reiniciar servicios Aplicar cambios
docker system prune Limpiar contenedores viejos Liberar espacio en disco

4.11. Solución de problemas comunes de Docker

❌ Problema 1: "Docker Desktop requires Windows 10 Pro/Enterprise"
⚠️ Causa: Versión muy antigua de Docker Desktop.
Solución:
• Las versiones nuevas de Docker (2020+) funcionan con Windows Home si tienes WSL 2
• Descarga la versión más reciente desde docker.com
• Asegúrate de tener WSL 2 instalado (sección 4.3 de esta guía)
❌ Problema 2: "WSL 2 installation is incomplete"
⚠️ Causa: WSL 2 no está actualizado o configurado.
Solución:
# Abre PowerShell como administrador y ejecuta: wsl --update wsl --set-default-version 2
# Luego reinicia Docker Desktop
❌ Problema 3: "Docker daemon is not running" o "Cannot connect to Docker"
⚠️ Causa: Docker Desktop no está iniciado.
Solución:
1. Busca Docker Desktop en el menú inicio y ábrelo
2. Espera a que aparezca "Engine running" en verde
3. Si no inicia, intenta reiniciar tu PC
4. Si persiste, desinstala y reinstala Docker Desktop
❌ Problema 4: Docker muy lento o se congela
⚠️ Causas comunes y soluciones:

• Poca RAM asignada: Aumenta en Settings → Resources → Advanced
• Antivirus bloqueando: Agrega una excepción para Docker en tu antivirus
• Disco lleno: Libera espacio en disco C:\ (necesitas al menos 10 GB libres)
• Demasiados contenedores: Ejecuta docker system prune -a
❌ Problema 5: "Error response from daemon: pull access denied"
⚠️ Causa: Problema de red o nombre de imagen incorrecto.
Solución:
1. Verifica tu conexión a internet
2. Verifica que escribiste bien el nombre de la imagen
3. Intenta con: docker pull hello-world para probar la conexión
❌ Problema 6: "Port is already allocated" (Puerto ya en uso)
⚠️ Causa: Ya tienes algo corriendo en ese puerto.
Solución:
1. Para MySQL (puerto 3306): Cierra MySQL local si lo tienes abierto
2. Para Django (puerto 8000): Detén otros servidores Django
3. O cambia el puerto en docker-compose.yml
¡Docker instalado, configurado y funcionando completamente!

Resumen de lo que lograste:
✅ WSL 2 instalado y configurado
✅ Docker Desktop instalado y corriendo
✅ Docker funcionando correctamente (probado con hello-world)
✅ Docker Compose disponible para orquestar servicios
✅ Sistema listo para desplegar el proyecto

Próximos pasos: Continúa con la siguiente sección para crear el proyecto Django. En la sección 16.6 aprenderás a usar Docker para desplegar todo el sistema (Django + MySQL + Nginx + Redis) con un solo comando.

Por ahora, solo necesitas que Docker Desktop esté instalado y corriendo. Puedes minimizar Docker Desktop, seguirá funcionando en segundo plano.

5. Creación del Proyecto Django

5.1. Crear carpeta del proyecto

1 Crear directorio de trabajo

1. Abre una terminal (CMD)
2. Navega a donde quieres crear el proyecto (ejemplo: Escritorio):

# Ir al escritorio cd Desktop
# Crear carpeta del proyecto mkdir django-sistema-u3
# Entrar a la carpeta cd django-sistema-u3

5.2. Crear entorno virtual

ℹ️ ¿Qué es un entorno virtual? Es como una "caja aislada" donde instalaremos las dependencias del proyecto sin afectar otras aplicaciones Python en tu computadora.
2 Crear y activar entorno virtual
# Crear entorno virtual llamado "venv311" py -3.11 -m venv venv311
# Activar el entorno virtual venv311\Scripts\activate
# Verifica versión del entorno virtual python --version

Cuando esté activado, verás (venv311) al inicio de cada línea en la terminal.

⚠️ Importante: Debes activar el entorno virtual cada vez que abras una nueva terminal para trabajar en este proyecto. Si no ves (venv311), ejecuta de nuevo venv311\Scripts\activate

5.3. Instalar dependencias

3 Instalar Django y todas las librerías necesarias

Asegúrate de que el entorno virtual esté activado (debe aparecer (venv311)), luego ejecuta:

# Actualizar pip primero python -m pip install --upgrade pip
# Instalar Django pip install django==5.2.10
# Instalar Django REST Framework pip install djangorestframework
# Instalar filtros para la API pip install django-filter
# Instalar conector de MySQL pip install pymysql mysqlclient
# Instalar Spyne para servicio SOAP pip install spyne lxml

Esto tomará unos minutos. Verás mucho texto desplazándose en la terminal. Espera hasta que termine y vuelva a aparecer (venv311).

¡Dependencias instaladas! Ya tenemos todas las librerías necesarias.

5.4. Crear proyecto Django

4 Crear el proyecto
# Crear proyecto Django llamado "biblioteca_project" django-admin startproject biblioteca_project .

El punto (.) al final es importante: indica que se cree en la carpeta actual.

5 Crear aplicación "libros"
# Crear app llamada "libros" python manage.py startapp libros
6 Crear archivo __init__.py para pymysql

Abre el archivo biblioteca_project/__init__.py con tu editor de texto (Notepad, VS Code, etc.) y agrega estas líneas:

biblioteca_project/__init__.py
import pymysql

pymysql.install_as_MySQLdb()

Guarda el archivo. Esto permite que Django use pymysql como conector de MySQL.

5. Estructura Completa del Proyecto

ℹ️ ¿Por qué necesitamos esta estructura? Django organiza el código en carpetas específicas para mantener todo ordenado y fácil de mantener.

5.1. Estructura actual (después de los pasos anteriores)

📁 django-sistema-u3/
├── 📁 venv311/ (entorno virtual)
├── 📁 biblioteca_project/ (configuración del proyecto)
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── 📁 libros/ (app principal)
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ ├── views.py
│ └── 📁 migrations/
└── manage.py (script principal de Django)

5.2. Crear carpetas adicionales necesarias

1 Crear carpetas para templates y archivos estáticos

Ejecuta estos comandos en la terminal (desde la carpeta django-sistema-u3):

# Crear carpeta para templates mkdir biblioteca_project\templates
# Crear carpetas para archivos estáticos mkdir static mkdir static\css mkdir static\js

5.3. Estructura final completa

📁 django-sistema-u3/
├── 📁 venv311/
├── 📁 biblioteca_project/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── views.py (crearemos este)
│ ├── wsgi.py
│ └── 📁 templates/ (templates HTML)
│ ├── home.html
│ ├── ejemplos_rest.html
│ ├── ejemplos_soap.html
│ └── ejemplos_admin.html
├── 📁 libros/
│ ├── __init__.py
│ ├── admin.py
│ ├── models.py (definiremos modelos)
│ ├── views.py (crearemos vistas)
│ ├── urls.py (crearemos este)
│ ├── serializers.py (crearemos este)
│ ├── soap_services.py (crearemos este)
│ └── 📁 migrations/
├── 📁 static/
│ ├── 📁 css/
│ │ └── styles.css
│ └── 📁 js/
│ └── main.js
├── manage.py
└── populate_db.py (crearemos este)

6. Configuración de Base de Datos MySQL

6.1. Configurar settings.py

1 Abrir y editar settings.py

1. Abre el archivo biblioteca_project/settings.py con tu editor de texto
2. Busca la sección INSTALLED_APPS (alrededor de la línea 33)
3. Agrega nuestras apps al final de la lista:

biblioteca_project/settings.py - INSTALLED_APPS
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # Apps de terceros
    'rest_framework',
    'django_filters',
    'spyne',
    
    # Apps del proyecto
    'libros',
]
2 Configurar la base de datos MySQL

En el mismo archivo settings.py, busca la sección DATABASES (alrededor de la línea 76) y reemplázala completamente con esto:

biblioteca_project/settings.py - DATABASES
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'biblioteca_dbutres',
        'USER': 'root',
        'PASSWORD': 'TU_CONTRASEÑA_MYSQL',  # ← Cambia esto por tu contraseña
        'HOST': 'localhost',
        'PORT': '3306',
        'OPTIONS': {
            'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
            'charset': 'utf8mb4',
        },
    }
}
⚠️ MUY IMPORTANTE: Reemplaza 'TU_CONTRASEÑA_MYSQL' con la contraseña que configuraste en MySQL (paso 3.3).

Ejemplo: Si tu contraseña es "mipassword123", debe quedar: 'PASSWORD': 'mipassword123',
3 Configurar zona horaria

Busca USE_TZ y TIME_ZONE en settings.py y modifícalos:

biblioteca_project/settings.py - Zona Horaria
USE_TZ = False
TIME_ZONE = 'America/Hermosillo'  # O tu zona horaria
4 Configurar directorios de templates

Busca la sección TEMPLATES en settings.py y modifica DIRS:

biblioteca_project/settings.py - TEMPLATES
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'biblioteca_project' / 'templates'],  # ← Agregar esta línea
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
5 Configurar archivos estáticos

Al final del archivo settings.py, busca STATIC_URL y agrega debajo:

biblioteca_project/settings.py - Archivos Estáticos
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
    BASE_DIR / 'static',
]
6 Configurar Django REST Framework

Al final del archivo settings.py, agrega esta configuración:

biblioteca_project/settings.py - REST Framework
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
}

Guarda el archivo settings.py.

¡Configuración completada! Django está configurado para usar MySQL.

7. Creación de Modelos

ℹ️ ¿Qué son los modelos? Son clases de Python que representan las tablas de la base de datos. Cada modelo se convierte en una tabla, y cada atributo en una columna.

7.1. Crear modelos en libros/models.py

1 Abrir y editar models.py

Abre el archivo libros/models.py y reemplaza todo su contenido con:

libros/models.py - CÓDIGO COMPLETO
from django.db import models
from django.contrib.auth.models import User

# Create your models here.

class Autor(models.Model):
    """Modelo para autores de libros"""
    nombre = models.CharField(max_length=100)
    apellido = models.CharField(max_length=100)
    fecha_nacimiento = models.DateField(null=True, blank=True)
    nacionalidad = models.CharField(max_length=50, blank=True)
    biografia = models.TextField(blank=True)
    
    class Meta:
        verbose_name = "Autor"
        verbose_name_plural = "Autores"
        ordering = ['apellido', 'nombre']
    
    def __str__(self):
        return f"{self.nombre} {self.apellido}"


class Editorial(models.Model):
    """Modelo para editoriales"""
    nombre = models.CharField(max_length=200)
    pais = models.CharField(max_length=50)
    sitio_web = models.URLField(blank=True)
    fecha_fundacion = models.DateField(null=True, blank=True)
    
    class Meta:
        verbose_name = "Editorial"
        verbose_name_plural = "Editoriales"
        ordering = ['nombre']
    
    def __str__(self):
        return self.nombre


class Categoria(models.Model):
    """Modelo para categorías de libros"""
    nombre = models.CharField(max_length=100, unique=True)
    descripcion = models.TextField(blank=True)
    
    class Meta:
        verbose_name = "Categoría"
        verbose_name_plural = "Categorías"
        ordering = ['nombre']
    
    def __str__(self):
        return self.nombre


class Libro(models.Model):
    """Modelo principal para libros"""
    ESTADO_CHOICES = [
        ('disponible', 'Disponible'),
        ('prestado', 'Prestado'),
        ('reservado', 'Reservado'),
        ('mantenimiento', 'En Mantenimiento'),
    ]
    
    titulo = models.CharField(max_length=200)
    isbn = models.CharField(max_length=13, unique=True)
    autor = models.ForeignKey(Autor, on_delete=models.CASCADE, related_name='libros')
    editorial = models.ForeignKey(Editorial, on_delete=models.SET_NULL, null=True, related_name='libros')
    categoria = models.ForeignKey(Categoria, on_delete=models.SET_NULL, null=True, related_name='libros')
    fecha_publicacion = models.DateField()
    numero_paginas = models.IntegerField()
    idioma = models.CharField(max_length=50, default='Español')
    descripcion = models.TextField(blank=True)
    estado = models.CharField(max_length=20, choices=ESTADO_CHOICES, default='disponible')
    stock_total = models.IntegerField(default=1)
    stock_disponible = models.IntegerField(default=1)
    ubicacion_fisica = models.CharField(max_length=50, blank=True, help_text="Ej: Estante A-12")
    fecha_registro = models.DateTimeField(auto_now_add=True)
    ultima_actualizacion = models.DateTimeField(auto_now=True)
    
    class Meta:
        verbose_name = "Libro"
        verbose_name_plural = "Libros"
        ordering = ['titulo']
    
    def __str__(self):
        return f"{self.titulo} - {self.autor}"
    
    def esta_disponible(self):
        """Verifica si el libro está disponible para préstamo"""
        return self.estado == 'disponible' and self.stock_disponible > 0


class Prestamo(models.Model):
    """Modelo para préstamos de libros"""
    ESTADO_CHOICES = [
        ('activo', 'Activo'),
        ('devuelto', 'Devuelto'),
        ('vencido', 'Vencido'),
        ('renovado', 'Renovado'),
    ]
    
    libro = models.ForeignKey(Libro, on_delete=models.CASCADE, related_name='prestamos')
    usuario = models.ForeignKey(User, on_delete=models.CASCADE, related_name='prestamos')
    fecha_prestamo = models.DateField(auto_now_add=True)
    fecha_devolucion_esperada = models.DateField()
    fecha_devolucion_real = models.DateField(null=True, blank=True)
    estado = models.CharField(max_length=20, choices=ESTADO_CHOICES, default='activo')
    renovaciones = models.IntegerField(default=0)
    multa = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
    notas = models.TextField(blank=True)
    
    class Meta:
        verbose_name = "Préstamo"
        verbose_name_plural = "Préstamos"
        ordering = ['-fecha_prestamo']
    
    def __str__(self):
        return f"{self.libro.titulo} - {self.usuario.username} ({self.estado})"
    
    def esta_vencido(self):
        """Verifica si el préstamo está vencido"""
        from datetime import date
        return self.estado == 'activo' and self.fecha_devolucion_esperada < date.today()

Guarda el archivo.

ℹ️ ¿Qué acabamos de crear?
Autor: Tabla para guardar información de autores
Categoria: Tabla para categorías de libros (Ficción, Historia, etc.)
Editorial: Tabla para editoriales
Libro: Tabla principal con toda la información de libros
Prestamo: Tabla para registrar préstamos de libros

8. Migraciones de Base de Datos

ℹ️ ¿Qué son las migraciones? Son archivos que Django crea para convertir nuestros modelos de Python en tablas reales en la base de datos MySQL.

8.1. Crear y aplicar migraciones

1 Crear archivos de migración

En la terminal (asegúrate de estar en la carpeta django-sistema-u3 y que el entorno virtual esté activado), ejecuta:

# Crear migraciones python manage.py makemigrations

Deberías ver algo como:

Migrations for 'libros': libros\migrations\0001_initial.py - Create model Autor - Create model Categoria - Create model Editorial - Create model Libro - Create model Prestamo
2 Aplicar migraciones a la base de datos
# Aplicar todas las migraciones python manage.py migrate

Esto creará todas las tablas en MySQL. Verás muchas líneas que dicen "OK" en verde.

3 Verificar las tablas en MySQL

Puedes verificar que las tablas se crearon correctamente:

# Abrir MySQL y ver las tablas mysql -u root -p USE biblioteca_dbutres; SHOW TABLES; exit;

Deberías ver tablas como: libros_autor, libros_libro, libros_prestamo, etc.

¡Tablas creadas exitosamente! La base de datos está lista.

9. Configuración de API REST

9.1. Crear serializers.py

1 Crear archivo serializers.py

Crea un nuevo archivo libros/serializers.py con este contenido:

libros/serializers.py
from rest_framework import serializers
from .models import Libro, Autor, Categoria, Editorial, Prestamo

class AutorSerializer(serializers.ModelSerializer):
    nombre_completo = serializers.CharField(read_only=True)
    
    class Meta:
        model = Autor
        fields = ['id', 'nombre', 'apellido', 'nombre_completo', 
                  'nacionalidad', 'biografia', 'fecha_nacimiento']

class CategoriaSerializer(serializers.ModelSerializer):
    class Meta:
        model = Categoria
        fields = ['id', 'nombre', 'descripcion']

class EditorialSerializer(serializers.ModelSerializer):
    class Meta:
        model = Editorial
        fields = ['id', 'nombre', 'pais', 'sitio_web']

class LibroSerializer(serializers.ModelSerializer):
    autor_nombre = serializers.CharField(source='autor.nombre_completo', read_only=True)
    categoria_nombre = serializers.CharField(source='categoria.nombre', read_only=True)
    editorial_nombre = serializers.CharField(source='editorial.nombre', read_only=True)
    
    class Meta:
        model = Libro
        fields = [
            'id', 'titulo', 'isbn', 'descripcion', 'fecha_publicacion',
            'numero_paginas', 'idioma', 'stock_total', 'stock_disponible',
            'estado', 'autor', 'autor_nombre', 'categoria', 'categoria_nombre',
            'editorial', 'editorial_nombre', 'fecha_creacion'
        ]
        read_only_fields = ['fecha_creacion']

class PrestamoSerializer(serializers.ModelSerializer):
    libro_titulo = serializers.CharField(source='libro.titulo', read_only=True)
    usuario_nombre = serializers.CharField(source='usuario.username', read_only=True)
    
    class Meta:
        model = Prestamo
        fields = [
            'id', 'libro', 'libro_titulo', 'usuario', 'usuario_nombre',
            'fecha_prestamo', 'fecha_devolucion_esperada', 'fecha_devolucion_real',
            'estado', 'observaciones'
        ]
        read_only_fields = ['fecha_prestamo']

9.2. Actualizar libros/views.py

2 Crear ViewSets para la API

Abre libros/views.py y reemplaza su contenido con:

libros/views.py
from django.shortcuts import render
from rest_framework import viewsets, filters
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend

from .models import Libro, Autor, Categoria, Editorial, Prestamo
from .serializers import (
    LibroSerializer, AutorSerializer, CategoriaSerializer,
    EditorialSerializer, PrestamoSerializer
)

class LibroViewSet(viewsets.ModelViewSet):
    """ViewSet para gestión de libros via API REST"""
    queryset = Libro.objects.all()
    serializer_class = LibroSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['categoria', 'autor', 'editorial', 'estado']
    search_fields = ['titulo', 'isbn', 'autor__nombre', 'autor__apellido']
    ordering_fields = ['titulo', 'fecha_publicacion', 'stock_disponible']
    ordering = ['titulo']

class AutorViewSet(viewsets.ModelViewSet):
    """ViewSet para gestión de autores"""
    queryset = Autor.objects.all()
    serializer_class = AutorSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    filter_backends = [filters.SearchFilter, filters.OrderingFilter]
    search_fields = ['nombre', 'apellido', 'nacionalidad']
    ordering_fields = ['nombre', 'apellido']
    ordering = ['apellido']

class CategoriaViewSet(viewsets.ModelViewSet):
    """ViewSet para gestión de categorías"""
    queryset = Categoria.objects.all()
    serializer_class = CategoriaSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

class EditorialViewSet(viewsets.ModelViewSet):
    """ViewSet para gestión de editoriales"""
    queryset = Editorial.objects.all()
    serializer_class = EditorialSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

class PrestamoViewSet(viewsets.ModelViewSet):
    """ViewSet para gestión de préstamos"""
    queryset = Prestamo.objects.all()
    serializer_class = PrestamoSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
    filterset_fields = ['estado', 'usuario', 'libro']
    ordering_fields = ['fecha_prestamo', 'fecha_devolucion_esperada']
    ordering = ['-fecha_prestamo']

9.3. Crear libros/urls.py

3 Configurar URLs de la API

Crea el archivo libros/urls.py con:

libros/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

# Router para API REST
router = DefaultRouter()
router.register(r'libros', views.LibroViewSet)
router.register(r'autores', views.AutorViewSet)
router.register(r'categorias', views.CategoriaViewSet)
router.register(r'editoriales', views.EditorialViewSet)
router.register(r'prestamos', views.PrestamoViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

10. Configuración de Servicio SOAP

10.1. Crear libros/soap_services.py

⚠️ IMPORTANTE - Archivo Actualizado y Sincronizado: Este servicio SOAP ha sido completamente sincronizado con el modelo Libro.

✅ Modelos SOAP actualizados:
1. LibroModel (para listas) ahora incluye:
• stock_total ✅
• fecha_registro ✅
• ultima_actualizacion ✅

2. LibroDetalladoModel (para detalles) ahora incluye:
• ultima_actualizacion ✅

✅ Todas las funciones actualizadas:
• listar_libros()
• obtener_libro()
• buscar_libros_por_titulo()
• buscar_libros_por_autor()
• buscar_libros_por_categoria()
• listar_libros_disponibles()

📝 NOTA: El código completo tiene más de 600 líneas. Si necesitas el archivo completo actualizado, cópialo desde:
libros/soap_services.py en tu proyecto.
1 Crear servicio SOAP completo

Crea el archivo libros/soap_services.py con el siguiente código completo:

libros/soap_services.py (Parte 1/3 - Modelos Complejos)
"""
Servicios SOAP para el Sistema de Biblioteca
"""
from spyne import Application, rpc, ServiceBase, Integer, Unicode, Boolean, DateTime, Array, ComplexModel
from spyne.protocol.soap import Soap11
from spyne.server.django import DjangoApplication
from django.views.decorators.csrf import csrf_exempt
from datetime import datetime, timedelta
from libros.models import Libro, Autor, Categoria, Editorial, Prestamo
from django.contrib.auth.models import User


# ===== MODELOS COMPLEJOS SOAP =====

class AutorModel(ComplexModel):
    """Modelo SOAP para Autor"""
    id = Integer
    nombre = Unicode
    apellido = Unicode
    nacionalidad = Unicode
    biografia = Unicode


class CategoriaModel(ComplexModel):
    """Modelo SOAP para Categoría"""
    id = Integer
    nombre = Unicode
    descripcion = Unicode


class EditorialModel(ComplexModel):
    """Modelo SOAP para Editorial"""
    id = Integer
    nombre = Unicode
    pais = Unicode
    sitio_web = Unicode


class LibroModel(ComplexModel):
    """Modelo SOAP para Libro"""
    id = Integer
    titulo = Unicode
    isbn = Unicode
    autor_nombre = Unicode
    editorial_nombre = Unicode
    categoria_nombre = Unicode
    fecha_publicacion = Unicode
    numero_paginas = Integer
    idioma = Unicode
    descripcion = Unicode
    estado = Unicode
    stock_total = Integer
    stock_disponible = Integer
    ubicacion_fisica = Unicode
    fecha_registro = DateTime
    ultima_actualizacion = DateTime


class LibroDetalladoModel(ComplexModel):
    """Modelo SOAP extendido para Libro con relaciones"""
    id = Integer
    titulo = Unicode
    isbn = Unicode
    numero_paginas = Integer
    idioma = Unicode
    descripcion = Unicode
    estado = Unicode
    stock_total = Integer
    stock_disponible = Integer
    ubicacion_fisica = Unicode
    fecha_publicacion = Unicode
    fecha_registro = DateTime
    ultima_actualizacion = DateTime
    # Objetos completos de relaciones
    autor = AutorModel
    editorial = EditorialModel
    categoria = CategoriaModel


class PrestamoModel(ComplexModel):
    """Modelo SOAP para Préstamo"""
    id = Integer
    libro_titulo = Unicode
    usuario_nombre = Unicode
    fecha_prestamo = DateTime
    fecha_devolucion_esperada = Unicode
    fecha_devolucion_real = Unicode
    estado = Unicode
    multa = Unicode


class ResultadoOperacion(ComplexModel):
    """Modelo para respuestas de operaciones"""
    exito = Boolean
    mensaje = Unicode
    id = Integer
libros/soap_services.py (Parte 2/3 - Servicios SOAP)
# ===== SERVICIOS SOAP =====

class BibliotecaService(ServiceBase):
    """
    Servicios SOAP para gestión de biblioteca digital
    """
    
    # ===== SERVICIOS DE LIBROS =====
    
    @rpc(Integer, _returns=LibroDetalladoModel)
    def obtener_libro(ctx, libro_id):
        """
        Obtiene información completa de un libro por ID
        """
        try:
            libro = Libro.objects.select_related('autor', 'editorial', 'categoria').get(id=libro_id)
            
            # Crear modelo de autor
            autor_model = AutorModel(
                id=libro.autor.id,
                nombre=libro.autor.nombre,
                apellido=libro.autor.apellido,
                nacionalidad=libro.autor.nacionalidad or '',
                biografia=libro.autor.biografia or ''
            )
            
            # Crear modelo de editorial
            editorial_model = EditorialModel(
                id=libro.editorial.id if libro.editorial else 0,
                nombre=libro.editorial.nombre if libro.editorial else 'Sin editorial',
                pais=libro.editorial.pais if libro.editorial else '',
                sitio_web=libro.editorial.sitio_web if libro.editorial else ''
            )
            
            # Crear modelo de categoría
            categoria_model = CategoriaModel(
                id=libro.categoria.id if libro.categoria else 0,
                nombre=libro.categoria.nombre if libro.categoria else 'Sin categoría',
                descripcion=libro.categoria.descripcion if libro.categoria else ''
            )
            
            return LibroDetalladoModel(
                id=libro.id,
                titulo=libro.titulo,
                isbn=libro.isbn,
                numero_paginas=libro.numero_paginas,
                idioma=libro.idioma,
                descripcion=libro.descripcion or '',
                estado=libro.estado,
                stock_total=libro.stock_total,
                stock_disponible=libro.stock_disponible,
                ubicacion_fisica=libro.ubicacion_fisica or '',
                fecha_publicacion=str(libro.fecha_publicacion),
                fecha_registro=libro.fecha_registro,
                ultima_actualizacion=libro.ultima_actualizacion,
                autor=autor_model,
                editorial=editorial_model,
                categoria=categoria_model
            )
        except Libro.DoesNotExist:
            return None
    
    @rpc(_returns=Array(LibroModel))
    def listar_libros(ctx):
        """Lista todos los libros disponibles"""
        libros = Libro.objects.select_related('autor', 'editorial', 'categoria').all()
        resultado = []
        
        for libro in libros:
            resultado.append(LibroModel(
                id=libro.id,
                titulo=libro.titulo,
                isbn=libro.isbn,
                autor_nombre=f"{libro.autor.nombre} {libro.autor.apellido}",
                editorial_nombre=libro.editorial.nombre if libro.editorial else 'Sin editorial',
                categoria_nombre=libro.categoria.nombre if libro.categoria else 'Sin categoría',
                fecha_publicacion=str(libro.fecha_publicacion),
                numero_paginas=libro.numero_paginas,
                idioma=libro.idioma,
                descripcion=libro.descripcion or '',
                estado=libro.estado,
                stock_total=libro.stock_total,
                stock_disponible=libro.stock_disponible,
                ubicacion_fisica=libro.ubicacion_fisica or '',
                fecha_registro=libro.fecha_registro,
                ultima_actualizacion=libro.ultima_actualizacion
            ))
        
        return resultado
    
    @rpc(Unicode, _returns=Array(LibroModel))
    def buscar_libros_por_titulo(ctx, titulo):
        """Busca libros por título (búsqueda parcial)"""
        libros = Libro.objects.filter(titulo__icontains=titulo).select_related('autor', 'editorial', 'categoria')
        resultado = []
        
        for libro in libros:
            resultado.append(LibroModel(
                id=libro.id,
                titulo=libro.titulo,
                isbn=libro.isbn,
                autor_nombre=f"{libro.autor.nombre} {libro.autor.apellido}",
                editorial_nombre=libro.editorial.nombre if libro.editorial else 'Sin editorial',
                categoria_nombre=libro.categoria.nombre if libro.categoria else 'Sin categoría',
                fecha_publicacion=str(libro.fecha_publicacion),
                numero_paginas=libro.numero_paginas,
                idioma=libro.idioma,
                descripcion=libro.descripcion or '',
                estado=libro.estado,
                stock_total=libro.stock_total,
                stock_disponible=libro.stock_disponible,
                ubicacion_fisica=libro.ubicacion_fisica or '',
                fecha_registro=libro.fecha_registro,
                ultima_actualizacion=libro.ultima_actualizacion
            ))
        
        return resultado
    
    @rpc(Unicode, _returns=Array(LibroModel))
    def buscar_libros_por_autor(ctx, autor_apellido):
        """Busca libros por apellido del autor"""
        libros = Libro.objects.filter(
            autor__apellido__icontains=autor_apellido
        ).select_related('autor', 'editorial', 'categoria')
        
        resultado = []
        for libro in libros:
            resultado.append(LibroModel(
                id=libro.id,
                titulo=libro.titulo,
                isbn=libro.isbn,
                autor_nombre=f"{libro.autor.nombre} {libro.autor.apellido}",
                editorial_nombre=libro.editorial.nombre if libro.editorial else 'Sin editorial',
                categoria_nombre=libro.categoria.nombre if libro.categoria else 'Sin categoría',
                fecha_publicacion=str(libro.fecha_publicacion),
                numero_paginas=libro.numero_paginas,
                idioma=libro.idioma,
                descripcion=libro.descripcion or '',
                estado=libro.estado,
                stock_total=libro.stock_total,
                stock_disponible=libro.stock_disponible,
                ubicacion_fisica=libro.ubicacion_fisica or '',
                fecha_registro=libro.fecha_registro,
                ultima_actualizacion=libro.ultima_actualizacion
            ))
        
        return resultado
    
    @rpc(_returns=Array(LibroModel))
    def listar_libros_disponibles(ctx):
        """Lista solo los libros disponibles para préstamo"""
        libros = Libro.objects.filter(
            estado='disponible',
            stock_disponible__gt=0
        ).select_related('autor', 'editorial', 'categoria')
        
        resultado = []
        for libro in libros:
            resultado.append(LibroModel(
                id=libro.id,
                titulo=libro.titulo,
                isbn=libro.isbn,
                autor_nombre=f"{libro.autor.nombre} {libro.autor.apellido}",
                editorial_nombre=libro.editorial.nombre if libro.editorial else 'Sin editorial',
                categoria_nombre=libro.categoria.nombre if libro.categoria else 'Sin categoría',
                fecha_publicacion=str(libro.fecha_publicacion),
                numero_paginas=libro.numero_paginas,
                idioma=libro.idioma,
                descripcion=libro.descripcion or '',
                estado=libro.estado,
                stock_total=libro.stock_total,
                stock_disponible=libro.stock_disponible,
                ubicacion_fisica=libro.ubicacion_fisica or '',
                fecha_registro=libro.fecha_registro,
                ultima_actualizacion=libro.ultima_actualizacion
            ))
        
        return resultado
    
    @rpc(Unicode, _returns=Array(LibroModel))
    def buscar_libros_por_categoria(ctx, categoria_nombre):
        """Busca libros por categoría"""
        libros = Libro.objects.filter(
            categoria__nombre__icontains=categoria_nombre
        ).select_related('autor', 'editorial', 'categoria')
        
        resultado = []
        for libro in libros:
            resultado.append(LibroModel(
                id=libro.id,
                titulo=libro.titulo,
                isbn=libro.isbn,
                autor_nombre=f"{libro.autor.nombre} {libro.autor.apellido}",
                editorial_nombre=libro.editorial.nombre if libro.editorial else 'Sin editorial',
                categoria_nombre=libro.categoria.nombre if libro.categoria else 'Sin categoría',
                fecha_publicacion=str(libro.fecha_publicacion),
                numero_paginas=libro.numero_paginas,
                idioma=libro.idioma,
                descripcion=libro.descripcion or '',
                estado=libro.estado,
                stock_total=libro.stock_total,
                stock_disponible=libro.stock_disponible,
                ubicacion_fisica=libro.ubicacion_fisica or '',
                fecha_registro=libro.fecha_registro,
                ultima_actualizacion=libro.ultima_actualizacion
            ))
        
        return resultado
libros/soap_services.py (Parte 3/3 - Préstamos y Config)
    # ===== SERVICIOS DE PRÉSTAMOS =====
    
    @rpc(Integer, Integer, Integer, _returns=ResultadoOperacion)
    def crear_prestamo(ctx, libro_id, usuario_id, dias_prestamo):
        """Crea un nuevo préstamo de libro"""
        try:
            libro = Libro.objects.get(id=libro_id)
            usuario = User.objects.get(id=usuario_id)
            
            # Verificar disponibilidad
            if not libro.esta_disponible():
                return ResultadoOperacion(
                    exito=False,
                    mensaje=f"El libro '{libro.titulo}' no está disponible",
                    id=0
                )
            
            # Calcular fecha de devolución
            fecha_devolucion = (datetime.now() + timedelta(days=dias_prestamo)).date()
            
            # Crear préstamo
            prestamo = Prestamo.objects.create(
                libro=libro,
                usuario=usuario,
                fecha_devolucion_esperada=fecha_devolucion,
                estado='activo'
            )
            
            # Actualizar stock del libro
            libro.stock_disponible -= 1
            if libro.stock_disponible == 0:
                libro.estado = 'prestado'
            libro.save()
            
            return ResultadoOperacion(
                exito=True,
                mensaje=f"Préstamo creado exitosamente. Devolver antes del {fecha_devolucion}",
                id=prestamo.id
            )
            
        except Libro.DoesNotExist:
            return ResultadoOperacion(exito=False, mensaje="Libro no encontrado", id=0)
        except User.DoesNotExist:
            return ResultadoOperacion(exito=False, mensaje="Usuario no encontrado", id=0)
        except Exception as e:
            return ResultadoOperacion(exito=False, mensaje=f"Error: {str(e)}", id=0)
    
    @rpc(Integer, _returns=ResultadoOperacion)
    def devolver_libro(ctx, prestamo_id):
        """Registra la devolución de un libro"""
        try:
            prestamo = Prestamo.objects.get(id=prestamo_id)
            
            if prestamo.estado != 'activo':
                return ResultadoOperacion(
                    exito=False,
                    mensaje="El préstamo ya fue devuelto o está inactivo",
                    id=prestamo_id
                )
            
            # Registrar devolución
            from datetime import date
            prestamo.fecha_devolucion_real = date.today()
            prestamo.estado = 'devuelto'
            
            # Calcular multa si está vencido
            if prestamo.esta_vencido():
                dias_retraso = (date.today() - prestamo.fecha_devolucion_esperada).days
                prestamo.multa = dias_retraso * 10.00  # $10 por día
                prestamo.estado = 'vencido'
            
            prestamo.save()
            
            # Actualizar stock del libro
            libro = prestamo.libro
            libro.stock_disponible += 1
            if libro.stock_disponible > 0:
                libro.estado = 'disponible'
            libro.save()
            
            mensaje = f"Libro devuelto exitosamente"
            if prestamo.multa > 0:
                mensaje += f". Multa: ${prestamo.multa}"
            
            return ResultadoOperacion(
                exito=True,
                mensaje=mensaje,
                id=prestamo_id
            )
            
        except Prestamo.DoesNotExist:
            return ResultadoOperacion(exito=False, mensaje="Préstamo no encontrado", id=0)
        except Exception as e:
            return ResultadoOperacion(exito=False, mensaje=f"Error: {str(e)}", id=0)
    
    @rpc(Integer, _returns=Array(PrestamoModel))
    def obtener_prestamos_usuario(ctx, usuario_id):
        """Obtiene todos los préstamos de un usuario"""
        prestamos = Prestamo.objects.filter(usuario_id=usuario_id).select_related('libro', 'usuario')
        resultado = []
        
        for prestamo in prestamos:
            resultado.append(PrestamoModel(
                id=prestamo.id,
                libro_titulo=prestamo.libro.titulo,
                usuario_nombre=prestamo.usuario.get_full_name() or prestamo.usuario.username,
                fecha_prestamo=prestamo.fecha_prestamo,
                fecha_devolucion_esperada=str(prestamo.fecha_devolucion_esperada),
                fecha_devolucion_real=str(prestamo.fecha_devolucion_real) if prestamo.fecha_devolucion_real else '',
                estado=prestamo.estado,
                multa=str(prestamo.multa)
            ))
        
        return resultado
    
    @rpc(_returns=Array(PrestamoModel))
    def listar_prestamos_activos(ctx):
        """Lista todos los préstamos activos"""
        prestamos = Prestamo.objects.filter(estado='activo').select_related('libro', 'usuario')
        resultado = []
        
        for prestamo in prestamos:
            resultado.append(PrestamoModel(
                id=prestamo.id,
                libro_titulo=prestamo.libro.titulo,
                usuario_nombre=prestamo.usuario.get_full_name() or prestamo.usuario.username,
                fecha_prestamo=prestamo.fecha_prestamo,
                fecha_devolucion_esperada=str(prestamo.fecha_devolucion_esperada),
                fecha_devolucion_real='',
                estado=prestamo.estado,
                multa=str(prestamo.multa)
            ))
        
        return resultado
    
    # ===== SERVICIOS DE AUTORES =====
    
    @rpc(_returns=Array(AutorModel))
    def listar_autores(ctx):
        """Lista todos los autores"""
        autores = Autor.objects.all()
        resultado = []
        
        for autor in autores:
            resultado.append(AutorModel(
                id=autor.id,
                nombre=autor.nombre,
                apellido=autor.apellido,
                nacionalidad=autor.nacionalidad or '',
                biografia=autor.biografia or ''
            ))
        
        return resultado
    
    # ===== SERVICIOS DE CATEGORÍAS =====
    
    @rpc(_returns=Array(CategoriaModel))
    def listar_categorias(ctx):
        """Lista todas las categorías"""
        categorias = Categoria.objects.all()
        resultado = []
        
        for categoria in categorias:
            resultado.append(CategoriaModel(
                id=categoria.id,
                nombre=categoria.nombre,
                descripcion=categoria.descripcion or ''
            ))
        
        return resultado


# ===== CONFIGURACIÓN DE LA APLICACIÓN SOAP =====

soap_app = Application(
    [BibliotecaService],
    tns='biblioteca.soap.services',
    in_protocol=Soap11(validator='lxml'),
    out_protocol=Soap11()
)

# Vista Django para el servicio SOAP
django_soap_application = csrf_exempt(DjangoApplication(soap_app))
ℹ️ Servicios SOAP incluidos:
Libros: obtener_libro, listar_libros, buscar por título/autor/categoría, listar disponibles
Préstamos: crear_prestamo, devolver_libro, obtener_prestamos_usuario, listar_prestamos_activos
Autores: listar_autores
Categorías: listar_categorias
⚠️ Importante: Este archivo contiene 552 líneas de código. Asegúrate de copiar el código completo en 3 partes consecutivas sin dejar espacios entre ellas.

11. Creación de Templates HTML

11.1. Crear views.py principal

1 Crear biblioteca_project/views.py

Crea el archivo biblioteca_project/views.py:

biblioteca_project/views.py
from django.shortcuts import render
from libros.models import Libro, Autor, Categoria, Prestamo

def home(request):
    """Vista de la página principal"""
    context = {
        'total_libros': Libro.objects.count(),
        'total_autores': Autor.objects.count(),
        'total_categorias': Categoria.objects.count(),
        'total_prestamos': Prestamo.objects.filter(estado='activo').count(),
    }
    return render(request, 'home.html', context)

def ejemplos_rest(request):
    return render(request, 'ejemplos_rest.html')

def ejemplos_soap(request):
    return render(request, 'ejemplos_soap.html')

def ejemplos_admin(request):
    return render(request, 'ejemplos_admin.html')

11.2. Crear template home.html

2 Crear página de inicio

Crea el archivo biblioteca_project/templates/home.html. Puedes obtener el código completo desde el archivo de templates.

ℹ️ Contenido: El archivo contiene home.html y todos los templates de ejemplos (ejemplos_rest.html, ejemplos_soap.html, ejemplos_admin.html).

11.3. Crear templates de ejemplos

3 Copiar templates

Ya tienes los templates de ejemplos creados en tu proyecto. Puedes acceder a ellos directamente desde estos botones:

📡 Ejemplos API REST
Ver ejemplos de endpoints
🔌 Ejemplos SOAP
Ver ejemplos del servicio SOAP
👨‍💼 Ejemplos Admin
Ver ejemplos del panel admin
ℹ️ Los archivos ya están en biblioteca_project/templates/

12. Archivos Estáticos (CSS y JavaScript)

12.1. Crear styles.css y main.js

1 Obtener archivos CSS y JavaScript

Los archivos CSS y JavaScript completos están disponibles en el siguiente archivo. Copia el contenido de styles.css a static/css/styles.css y main.js a static/js/main.js

ℹ️ Contenido: El archivo incluye el CSS completo (500+ líneas) con todos los estilos del sistema y el JavaScript (200+ líneas) con AJAX, animaciones y validaciones.

12.2. Crear las carpetas y archivos

2 Estructura de archivos estáticos

Asegúrate de crear los archivos en la ubicación correcta:

  • static/css/styles.css - Estilos del sistema
  • static/js/main.js - JavaScript interactivo

12.3. Recolectar archivos estáticos

3 Ejecutar collectstatic

Una vez que hayas copiado los archivos CSS y JS, ejecuta:

python manage.py collectstatic --noinput

Este comando recopila todos los archivos estáticos en la carpeta staticfiles/

13. Configuración del Panel Admin

13.1. Crear admin.py

1 Configurar panel administrativo

Abre libros/admin.py y reemplaza su contenido con:

libros/admin.py
from django.contrib import admin
from .models import Autor, Categoria, Editorial, Libro, Prestamo

@admin.register(Autor)
class AutorAdmin(admin.ModelAdmin):
    list_display = ['nombre', 'apellido', 'nacionalidad', 'fecha_nacimiento']
    search_fields = ['nombre', 'apellido', 'nacionalidad']
    list_filter = ['nacionalidad']

@admin.register(Categoria)
class CategoriaAdmin(admin.ModelAdmin):
    list_display = ['nombre', 'descripcion']
    search_fields = ['nombre']

@admin.register(Editorial)
class EditorialAdmin(admin.ModelAdmin):
    list_display = ['nombre', 'pais', 'sitio_web']
    search_fields = ['nombre', 'pais']
    list_filter = ['pais']

@admin.register(Libro)
class LibroAdmin(admin.ModelAdmin):
    list_display = ['titulo', 'autor', 'categoria', 'stock_disponible', 'estado']
    search_fields = ['titulo', 'isbn', 'autor__nombre', 'autor__apellido']
    list_filter = ['categoria', 'editorial', 'estado', 'idioma']
    readonly_fields = ['fecha_registro']

@admin.register(Prestamo)
class PrestamoAdmin(admin.ModelAdmin):
    list_display = ['libro', 'usuario', 'fecha_prestamo', 
                    'fecha_devolucion_esperada', 'estado']
    search_fields = ['libro__titulo', 'usuario__username']
    list_filter = ['estado', 'fecha_prestamo']
    readonly_fields = ['fecha_prestamo']

13.2. Crear superusuario

2 Crear cuenta de administrador
python manage.py createsuperuser

Te pedirá:
Username: admin
Email: admin@biblioteca.com (o déjalo en blanco)
Password: admin123 (escríbelo dos veces)

⚠️ Nota: La contraseña no se verá mientras la escribes. Es normal.

14. Configuración de URLs

1 Configurar biblioteca_project/urls.py

Abre biblioteca_project/urls.py y reemplaza su contenido con:

biblioteca_project/urls.py
from django.contrib import admin
from django.urls import path, include, re_path
from libros.soap_services import django_soap_application
from . import views

urlpatterns = [
    # Página principal
    path('', views.home, name='home'),
    
    # Páginas de ejemplos
    path('ejemplos/rest/', views.ejemplos_rest, name='ejemplos_rest'),
    path('ejemplos/soap/', views.ejemplos_soap, name='ejemplos_soap'),
    path('ejemplos/admin/', views.ejemplos_admin, name='ejemplos_admin'),
    
    # Admin
    path('admin/', admin.site.urls),
    
    # Servicio SOAP
    re_path(r'^soap/', django_soap_application),
    
    # API REST
    path('api/', include('libros.urls')),
]

15. Población de Datos de Prueba

15.1. Crear script populate_db.py

⚠️ IMPORTANTE - Archivo Actualizado: Este archivo ha sido sincronizado con todos los atributos del modelo Libro.

✅ Campos incluidos en cada libro:
• titulo, isbn, autor, editorial, categoria
• fecha_publicacion, numero_paginas, idioma, descripcion
estado='disponible' (Campo crítico agregado)
• stock_total, stock_disponible, ubicacion_fisica
• fecha_registro y ultima_actualizacion (auto-generados por Django)
1 Crear script de población

Crea el archivo populate_db.py en la raíz del proyecto:

populate_db.py - CÓDIGO COMPLETO
"""
Script para poblar la base de datos con datos de prueba
Ejecutar con: python populate_db.py
"""
import os
import django
from datetime import date, timedelta

# Configurar Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'biblioteca_project.settings')
django.setup()

from django.contrib.auth.models import User
from libros.models import Autor, Editorial, Categoria, Libro, Prestamo


def crear_usuarios():
    """Crear usuarios de prueba"""
    print("Creando usuarios...")
    
    # Crear superusuario si no existe
    if not User.objects.filter(username='admin').exists():
        User.objects.create_superuser(
            username='admin',
            email='admin@biblioteca.com',
            password='admin123',
            first_name='Administrador',
            last_name='Sistema'
        )
        print("  ✓ Superusuario 'admin' creado (password: admin123)")
    
    # Crear usuarios normales
    usuarios_data = [
        {'username': 'juan_perez', 'email': 'juan@email.com', 'first_name': 'Juan', 'last_name': 'Pérez'},
        {'username': 'maria_lopez', 'email': 'maria@email.com', 'first_name': 'María', 'last_name': 'López'},
        {'username': 'carlos_ruiz', 'email': 'carlos@email.com', 'first_name': 'Carlos', 'last_name': 'Ruiz'},
    ]
    
    for user_data in usuarios_data:
        if not User.objects.filter(username=user_data['username']).exists():
            User.objects.create_user(
                password='user123',
                **user_data
            )
            print(f"  ✓ Usuario '{user_data['username']}' creado")


def crear_autores():
    """Crear autores de prueba"""
    print("\nCreando autores...")
    
    autores_data = [
        {
            'nombre': 'Gabriel',
            'apellido': 'García Márquez',
            'fecha_nacimiento': date(1927, 3, 6),
            'nacionalidad': 'Colombiano',
            'biografia': 'Premio Nobel de Literatura 1982. Autor de Cien años de soledad.'
        },
        {
            'nombre': 'Isabel',
            'apellido': 'Allende',
            'fecha_nacimiento': date(1942, 8, 2),
            'nacionalidad': 'Chilena',
            'biografia': 'Una de las novelistas más leídas en español. Autora de La casa de los espíritus.'
        },
        {
            'nombre': 'Jorge Luis',
            'apellido': 'Borges',
            'fecha_nacimiento': date(1899, 8, 24),
            'nacionalidad': 'Argentino',
            'biografia': 'Uno de los escritores más importantes del siglo XX en lengua española.'
        },
        {
            'nombre': 'Octavio',
            'apellido': 'Paz',
            'fecha_nacimiento': date(1914, 3, 31),
            'nacionalidad': 'Mexicano',
            'biografia': 'Premio Nobel de Literatura 1990. Ensayista y poeta mexicano.'
        },
        {
            'nombre': 'Mario',
            'apellido': 'Vargas Llosa',
            'fecha_nacimiento': date(1936, 3, 28),
            'nacionalidad': 'Peruano',
            'biografia': 'Premio Nobel de Literatura 2010. Autor de La ciudad y los perros.'
        },
    ]
    
    for autor_data in autores_data:
        autor, created = Autor.objects.get_or_create(
            nombre=autor_data['nombre'],
            apellido=autor_data['apellido'],
            defaults=autor_data
        )
        if created:
            print(f"  ✓ Autor '{autor}' creado")


def crear_editoriales():
    """Crear editoriales de prueba"""
    print("\nCreando editoriales...")
    
    editoriales_data = [
        {
            'nombre': 'Editorial Sudamericana',
            'pais': 'Argentina',
            'sitio_web': 'https://www.megustaleer.com.ar',
            'fecha_fundacion': date(1939, 1, 1)
        },
        {
            'nombre': 'Planeta',
            'pais': 'España',
            'sitio_web': 'https://www.planetadelibros.com',
            'fecha_fundacion': date(1949, 1, 1)
        },
        {
            'nombre': 'Alfaguara',
            'pais': 'España',
            'sitio_web': 'https://www.penguinrandomhouse.com',
            'fecha_fundacion': date(1964, 1, 1)
        },
        {
            'nombre': 'Anagrama',
            'pais': 'España',
            'sitio_web': 'https://www.anagrama-ed.es',
            'fecha_fundacion': date(1969, 1, 1)
        },
    ]
    
    for editorial_data in editoriales_data:
        editorial, created = Editorial.objects.get_or_create(
            nombre=editorial_data['nombre'],
            defaults=editorial_data
        )
        if created:
            print(f"  ✓ Editorial '{editorial}' creada")


def crear_categorias():
    """Crear categorías de prueba"""
    print("\nCreando categorías...")
    
    categorias_data = [
        {'nombre': 'Ficción', 'descripcion': 'Novelas y cuentos de ficción literaria'},
        {'nombre': 'Fantasía', 'descripcion': 'Literatura fantástica y de mundos imaginarios'},
        {'nombre': 'Ciencia Ficción', 'descripcion': 'Narrativa especulativa y futurista'},
        {'nombre': 'Romance', 'descripcion': 'Novelas románticas y de amor'},
        {'nombre': 'Misterio', 'descripcion': 'Novelas policiacas y de suspenso'},
        {'nombre': 'Terror', 'descripcion': 'Literatura de horror y terror'},
        {'nombre': 'Aventura', 'descripcion': 'Historias de aventuras y acción'},
        {'nombre': 'Historia', 'descripcion': 'Libros de historia y biografías'},
        {'nombre': 'Poesía', 'descripcion': 'Obras poéticas y antologías'},
        {'nombre': 'Ensayo', 'descripcion': 'Ensayos literarios y filosóficos'},
    ]
    
    for categoria_data in categorias_data:
        categoria, created = Categoria.objects.get_or_create(
            nombre=categoria_data['nombre'],
            defaults=categoria_data
        )
        if created:
            print(f"  ✓ Categoría '{categoria}' creada")


def crear_libros():
    """Crear libros de prueba"""
    print("\nCreando libros...")
    
    # Obtener datos existentes
    garcia_marquez = Autor.objects.get(apellido='García Márquez')
    allende = Autor.objects.get(apellido='Allende')
    borges = Autor.objects.get(apellido='Borges')
    paz = Autor.objects.get(apellido='Paz')
    vargas_llosa = Autor.objects.get(apellido='Vargas Llosa')
    
    sudamericana = Editorial.objects.get(nombre='Editorial Sudamericana')
    planeta = Editorial.objects.get(nombre='Planeta')
    alfaguara = Editorial.objects.get(nombre='Alfaguara')
    
    ficcion = Categoria.objects.get(nombre='Ficción')
    poesia = Categoria.objects.get(nombre='Poesía')
    ensayo = Categoria.objects.get(nombre='Ensayo')
    
    libros_data = [
        {
            'titulo': 'Cien años de soledad',
            'isbn': '9780307474728',
            'autor': garcia_marquez,
            'editorial': sudamericana,
            'categoria': ficcion,
            'fecha_publicacion': date(1967, 5, 30),
            'numero_paginas': 471,
            'idioma': 'Español',
            'descripcion': 'Obra maestra del realismo mágico que narra la historia de la familia Buendía.',
            'estado': 'disponible',
            'stock_total': 5,
            'stock_disponible': 3,
            'ubicacion_fisica': 'Estante A-12'
        },
        {
            'titulo': 'El amor en los tiempos del cólera',
            'isbn': '9780307387738',
            'autor': garcia_marquez,
            'editorial': sudamericana,
            'categoria': ficcion,
            'fecha_publicacion': date(1985, 1, 1),
            'numero_paginas': 368,
            'idioma': 'Español',
            'descripcion': 'Historia de amor que transcurre a lo largo de más de cincuenta años.',
            'estado': 'disponible',
            'stock_total': 3,
            'stock_disponible': 2,
            'ubicacion_fisica': 'Estante A-13'
        },
        {
            'titulo': 'La casa de los espíritus',
            'isbn': '9788401242281',
            'autor': allende,
            'editorial': planeta,
            'categoria': ficcion,
            'fecha_publicacion': date(1982, 1, 1),
            'numero_paginas': 433,
            'idioma': 'Español',
            'descripcion': 'Saga familiar chilena que mezcla lo cotidiano con lo maravilloso.',
            'estado': 'disponible',
            'stock_total': 4,
            'stock_disponible': 4,
            'ubicacion_fisica': 'Estante B-05'
        },
        {
            'titulo': 'Ficciones',
            'isbn': '9780802130303',
            'autor': borges,
            'editorial': sudamericana,
            'categoria': ficcion,
            'fecha_publicacion': date(1944, 1, 1),
            'numero_paginas': 174,
            'idioma': 'Español',
            'descripcion': 'Colección de cuentos que explora temas filosóficos y metafísicos.',
            'estado': 'disponible',
            'stock_total': 3,
            'stock_disponible': 1,
            'ubicacion_fisica': 'Estante C-08'
        },
        {
            'titulo': 'El laberinto de la soledad',
            'isbn': '9786071613578',
            'autor': paz,
            'editorial': sudamericana,
            'categoria': ensayo,
            'fecha_publicacion': date(1950, 1, 1),
            'numero_paginas': 191,
            'idioma': 'Español',
            'descripcion': 'Ensayo sobre la identidad mexicana y latinoamericana.',
            'estado': 'disponible',
            'stock_total': 2,
            'stock_disponible': 2,
            'ubicacion_fisica': 'Estante D-15'
        },
        {
            'titulo': 'La ciudad y los perros',
            'isbn': '9788420412146',
            'autor': vargas_llosa,
            'editorial': alfaguara,
            'categoria': ficcion,
            'fecha_publicacion': date(1963, 1, 1),
            'numero_paginas': 399,
            'idioma': 'Español',
            'descripcion': 'Novela ambientada en un colegio militar de Lima.',
            'estado': 'disponible',
            'stock_total': 4,
            'stock_disponible': 3,
            'ubicacion_fisica': 'Estante E-20'
        },
        {
            'titulo': 'Conversación en La Catedral',
            'isbn': '9788420412153',
            'autor': vargas_llosa,
            'editorial': alfaguara,
            'categoria': ficcion,
            'fecha_publicacion': date(1969, 1, 1),
            'numero_paginas': 729,
            'idioma': 'Español',
            'descripcion': 'Retrato crítico de la sociedad peruana bajo dictadura.',
            'estado': 'disponible',
            'stock_total': 2,
            'stock_disponible': 2,
            'ubicacion_fisica': 'Estante E-21'
        },
    ]
    
    for libro_data in libros_data:
        libro, created = Libro.objects.get_or_create(
            isbn=libro_data['isbn'],
            defaults=libro_data
        )
        if created:
            print(f"  ✓ Libro '{libro.titulo}' creado")


def crear_prestamos():
    """Crear préstamos de prueba"""
    print("\nCreando préstamos...")
    
    # Obtener usuarios y libros
    juan = User.objects.get(username='juan_perez')
    maria = User.objects.get(username='maria_lopez')
    
    cien_anos = Libro.objects.get(isbn='9780307474728')
    ficciones = Libro.objects.get(isbn='9780802130303')
    
    # Crear préstamos
    prestamos_data = [
        {
            'libro': cien_anos,
            'usuario': juan,
            'fecha_devolucion_esperada': date.today() + timedelta(days=14),
            'estado': 'activo'
        },
        {
            'libro': ficciones,
            'usuario': maria,
            'fecha_devolucion_esperada': date.today() + timedelta(days=7),
            'estado': 'activo'
        },
    ]
    
    for prestamo_data in prestamos_data:
        prestamo, created = Prestamo.objects.get_or_create(
            libro=prestamo_data['libro'],
            usuario=prestamo_data['usuario'],
            estado='activo',
            defaults=prestamo_data
        )
        if created:
            # Actualizar stock del libro
            libro = prestamo_data['libro']
            libro.stock_disponible -= 1
            if libro.stock_disponible == 0:
                libro.estado = 'prestado'
            libro.save()
            print(f"  ✓ Préstamo '{prestamo}' creado")


def main():
    """Función principal"""
    print("="*60)
    print("📚 POBLANDO BASE DE DATOS - Sistema de Biblioteca")
    print("="*60)
    
    try:
        crear_usuarios()
        crear_autores()
        crear_editoriales()
        crear_categorias()
        crear_libros()
        crear_prestamos()
        
        print("\n" + "="*60)
        print("✅ BASE DE DATOS POBLADA EXITOSAMENTE")
        print("="*60)
        print("\n📊 Resumen:")
        print(f"  • Usuarios: {User.objects.count()}")
        print(f"  • Autores: {Autor.objects.count()}")
        print(f"  • Editoriales: {Editorial.objects.count()}")
        print(f"  • Categorías: {Categoria.objects.count()}")
        print(f"  • Libros: {Libro.objects.count()}")
        print(f"  • Préstamos: {Prestamo.objects.count()}")
        print("\n🔑 Credenciales de acceso:")
        print("  Admin: username='admin', password='admin123'")
        print("  Usuarios: password='user123'")
        print("\n🌐 Accede al panel de administración en:")
        print("  http://localhost:8000/admin/")
        
    except Exception as e:
        print(f"\n❌ ERROR: {e}")
        import traceback
        traceback.print_exc()


if __name__ == '__main__':
    main()

15.2. Ejecutar el script

2 Poblar la base de datos
python populate_db.py

Esto creará libros, autores, categorías y usuarios de prueba.

¡Datos de prueba creados! La base de datos tiene contenido para probar.

16. Ejecución y Pruebas del Sistema

16.1. Iniciar el servidor

1 Ejecutar el servidor de desarrollo
python manage.py runserver

Deberías ver algo como:

Django version 5.2.10, using settings 'biblioteca_project.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CTRL-BREAK.

16.2. Probar las páginas

2 Abrir el navegador y probar

Abre tu navegador y visita estas URLs:

Página URL Descripción
🏠 Inicio http://127.0.0.1:8000/ Página principal del sistema
👨‍💼 Admin http://127.0.0.1:8000/admin/ Panel administrativo de Django
🔌 API REST http://127.0.0.1:8000/api/ API REST browsable
📚 Libros API http://127.0.0.1:8000/api/libros/ Endpoint de libros
🧼 SOAP WSDL http://127.0.0.1:8000/soap/?wsdl Definición del servicio SOAP
📖 Ejemplos REST http://127.0.0.1:8000/ejemplos/rest/ Ejemplos de uso de API REST
🧼 Ejemplos SOAP http://127.0.0.1:8000/ejemplos/soap/ Ejemplos de uso de SOAP

16.3. Probar el panel admin

3 Acceder al administrador

1. Ve a http://127.0.0.1:8000/admin/
2. Usuario: admin
3. Contraseña: admin123
4. Explora las secciones de Autores, Libros, Préstamos, etc.

16.4. Probar la API REST

4 Usar la API desde el navegador

Django REST Framework incluye una interfaz web para probar la API:

Listar libros: http://127.0.0.1:8000/api/libros/
Ver un libro: http://127.0.0.1:8000/api/libros/1/
Buscar libros: http://127.0.0.1:8000/api/libros/?search=garcia
Filtrar por categoría: http://127.0.0.1:8000/api/libros/?categoria=1

¡Sistema funcionando! Todas las funcionalidades están operativas.

16.5. Pruebas del Servicio SOAP (cliente_soap_visual.py)

ℹ️ ¿Qué es el cliente SOAP interactivo? Es un script de Python que te permite probar todas las operaciones SOAP del sistema de manera fácil e interactiva desde la terminal.

16.5.1. Instalar dependencia zeep

1 Instalar la librería zeep (cliente SOAP)

Con el entorno virtual activado, instala zeep:

# Asegúrate de que el entorno virtual esté activado venv311\Scripts\activate
# Instalar zeep pip install zeep

16.5.2. Verificar que el servidor esté corriendo

2 Iniciar el servidor Django

En una terminal, asegúrate de que el servidor Django esté corriendo:

python manage.py runserver
⚠️ Importante: Deja esta terminal abierta con el servidor corriendo. El cliente SOAP necesita conectarse al servidor.

16.5.3. Ejecutar el cliente SOAP interactivo

3 Abrir una segunda terminal y ejecutar el cliente

1. Abre una NUEVA terminal (CMD)
2. Navega a la carpeta del proyecto:

cd django-sistema-u3
# Activar el entorno virtual venv311\Scripts\activate
# Ejecutar el cliente SOAP python cliente_soap_visual.py

16.5.4. Usar el menú interactivo

4 Explorar las opciones del menú

El cliente mostrará un menú como este:

Menú del Cliente SOAP
================================================================================
🏛️  SISTEMA DE BIBLIOTECA - CLIENTE SOAP INTERACTIVO
================================================================================

📚 OPERACIONES DE LIBROS:
  1. Listar todos los libros
  2. Obtener libro por ID
  3. Buscar libros por título
  4. Buscar libros por autor (apellido)
  5. Buscar libros por categoría
  6. Listar libros disponibles

📋 OPERACIONES DE PRÉSTAMOS:
  7. Crear préstamo
  8. Devolver libro
  9. Ver préstamos de un usuario
 10. Listar préstamos activos

👥 OPERACIONES DE CATÁLOGOS:
 11. Listar autores
 12. Listar categorías

  0. Salir
================================================================================

16.5.5. Ejemplos de uso paso a paso

Ejemplo 1: Listar todos los libros

1. Selecciona la opción 1
2. Presiona Enter
3. El sistema mostrará todos los libros con ID, título, autor y stock

Salida esperada:
📚 LISTANDO TODOS LOS LIBROS
--------------------------------------------------------------------------------
Total de libros: 7

ID:   1 | Cien años de soledad                       | Gabriel García Márquez    | Stock: 3
ID:   2 | El amor en los tiempos del cólera          | Gabriel García Márquez    | Stock: 2
ID:   3 | La casa de los espíritus                   | Isabel Allende            | Stock: 4
...

✅ Operación completada
Ejemplo 2: Buscar libro por título

1. Selecciona la opción 3
2. Ingresa parte del título, por ejemplo: soledad
3. Presiona Enter

Salida esperada:
🔍 BUSCAR LIBROS POR TÍTULO
--------------------------------------------------------------------------------
Ingrese el título o parte del título a buscar: soledad

✅ Se encontraron 1 libro(s):

ID:   1 | Cien años de soledad
       Autor: Gabriel García Márquez           | Stock: 3
--------------------------------------------------------------------------------
Ejemplo 3: Obtener detalles completos de un libro

1. Selecciona la opción 2
2. Ingresa el ID del libro, por ejemplo: 1
3. Presiona Enter

Salida esperada:
🔍 OBTENER LIBRO POR ID
--------------------------------------------------------------------------------
Ingrese el ID del libro: 1

📖 INFORMACIÓN DEL LIBRO:
  ID: 1
  Título: Cien años de soledad
  ISBN: 9780307474728
  Autor: Gabriel García Márquez
  Nacionalidad autor: Colombiano
  Editorial: Editorial Sudamericana
  País editorial: Argentina
  Categoría: Ficción
  Fecha publicación: 1967-05-30
  Páginas: 471
  Idioma: Español
  Estado: disponible
  Stock total: 5
  Stock disponible: 3
  Ubicación: Estante A-12
  Descripción: Obra maestra del realismo mágico...

✅ Libro encontrado
Ejemplo 4: Buscar libros por autor

1. Selecciona la opción 4
2. Ingresa el apellido del autor: García Márquez
3. Presiona Enter

Salida esperada:
👤 BUSCAR LIBROS POR AUTOR
--------------------------------------------------------------------------------
Ingrese el apellido del autor: García Márquez

✅ Se encontraron 2 libro(s) del autor 'García Márquez':

ID:   1 | Cien años de soledad
       Autor: Gabriel García Márquez           | Stock: 3
       Editorial: Editorial Sudamericana      | Categoría: Ficción
--------------------------------------------------------------------------------
ID:   2 | El amor en los tiempos del cólera
       Autor: Gabriel García Márquez           | Stock: 2
       Editorial: Editorial Sudamericana      | Categoría: Ficción
--------------------------------------------------------------------------------
Ejemplo 5: Crear un préstamo

1. Selecciona la opción 7
2. Ingresa el ID del libro: 3
3. Ingresa el ID del usuario: 2
4. Ingresa los días de préstamo: 14
5. Presiona Enter

Salida esperada:
➕ CREAR PRÉSTAMO
--------------------------------------------------------------------------------
Ingrese el ID del libro: 3
Ingrese el ID del usuario: 2
Días de préstamo (7, 14, 21, 30): 14

✅ PRÉSTAMO CREADO EXITOSAMENTE
  ID Préstamo: 3
  Mensaje: Préstamo creado exitosamente
Ejemplo 6: Listar préstamos activos

1. Selecciona la opción 10
2. Presiona Enter

Salida esperada:
📋 PRÉSTAMOS ACTIVOS
--------------------------------------------------------------------------------
Total de préstamos activos: 3

ID:   1 | Cien años de soledad
       Usuario: juan_perez
       Prestado: 2026-02-02 | Devolver: 2026-02-16
--------------------------------------------------------------------------------
ID:   2 | Ficciones
       Usuario: maria_lopez
       Prestado: 2026-02-02 | Devolver: 2026-02-09
--------------------------------------------------------------------------------

16.5.6. Todas las operaciones disponibles

Operación Descripción Datos requeridos
1. Listar libros Muestra todos los libros del sistema Ninguno
2. Obtener libro Muestra detalles completos de un libro ID del libro
3. Buscar por título Busca libros que contengan el texto Parte del título
4. Buscar por autor Busca libros de un autor Apellido del autor
5. Buscar por categoría Busca libros de una categoría Nombre de categoría
6. Libros disponibles Solo libros con stock > 0 Ninguno
7. Crear préstamo Registra un nuevo préstamo ID libro, ID usuario, días
8. Devolver libro Marca préstamo como devuelto ID del préstamo
9. Préstamos de usuario Historial de préstamos ID del usuario
10. Préstamos activos Todos los préstamos no devueltos Ninguno
11. Listar autores Muestra todos los autores Ninguno
12. Listar categorías Muestra todas las categorías Ninguno

16.5.7. Tips para usar el cliente SOAP

💡 Consejos útiles:

• Siempre mantén el servidor Django corriendo en otra terminal
• Usa IDs válidos (puedes verlos con las opciones de listar)
• Para salir del cliente, selecciona la opción 0
• Si hay un error, verifica que el servidor esté corriendo
• Puedes usar Ctrl+C para cancelar una operación
• El cliente muestra mensajes de error descriptivos

16.5.8. Verificar el WSDL directamente

Ver la definición WSDL en el navegador

También puedes ver la definición WSDL del servicio en:

http://127.0.0.1:8000/soap/?wsdl

Esto te mostrará el XML con la especificación completa del servicio SOAP.

¡Cliente SOAP funcionando! Ahora puedes probar todas las operaciones del servicio SOAP de forma interactiva. Esto demuestra que tu servicio web SOAP está completamente funcional y puede ser consumido por clientes externos.

16.6. Pruebas de APIs REST y SOAP con Postman

ℹ️ ¿Qué es Postman? Es la herramienta profesional más utilizada para probar, documentar y desarrollar APIs. Permite enviar peticiones HTTP/SOAP y ver las respuestas de manera visual y organizada.

16.6.1. Instalación de Postman

1 Descargar Postman

Opción A - Descarga directa (Recomendada):
1. Ve a: https://www.postman.com/downloads/
2. Haz clic en el botón azul "Download" para Windows
3. Se descargará el instalador (Postman-win64-Setup.exe, ~200 MB)
4. Espera a que termine la descarga

2 Instalar Postman

1. Ejecuta el archivo descargado (Postman-win64-Setup.exe)
2. La instalación comenzará automáticamente
3. Espera 2-3 minutos mientras se instala
4. Postman se abrirá automáticamente al terminar

16.6.2. Crear cuenta en Postman (Obligatorio)

⚠️ REQUISITO OBLIGATORIO: El nombre de usuario de tu cuenta Postman DEBE contener tus apellidos. Ejemplo: juan_garcia_lopez o garcia.lopez.api
1 Crear cuenta con tus apellidos

Cuando Postman se abra por primera vez, verás una pantalla de bienvenida:

  1. Haz clic en "Create Account" o "Sign Up"
  2. Completa el formulario:
    Email: Tu correo electrónico
    Username: TUS_APELLIDOS_API (ejemplo: garcia_lopez_api)
    Password: Una contraseña segura
  3. Marca la casilla de aceptar términos y condiciones
  4. Haz clic en "Create free account"
  5. Verifica tu email (revisa tu bandeja de entrada y spam)
  6. Haz clic en el enlace de verificación del correo
💡 Ejemplo de nombres de usuario válidos:
• garcia_lopez_api
• ramirez.martinez.dev
• hernandez_gonzalez_2024
• lopez-sanchez-sw
2 Configuración inicial

Después de iniciar sesión:

  1. Te preguntará sobre tu rol → Selecciona "Student" o "Developer"
  2. Te preguntará qué quieres hacer → Selecciona "Test APIs"
  3. Puedes omitir el tutorial (Skip) o hacerlo si lo deseas
  4. Ya estás en la pantalla principal de Postman ✅

16.6.3. Crear Workspace para el proyecto

1 Crear nuevo Workspace

1. En la esquina superior izquierda, haz clic en "Workspaces"
2. Haz clic en "Create Workspace"
3. Completa:

Name: Sistema Biblioteca U3
Summary: APIs REST y SOAP para Sistema de Biblioteca
Visibility: Personal (solo tú puedes verlo)

4. Haz clic en "Create Workspace"

2 Crear Collection para las APIs

1. En la barra lateral izquierda, haz clic en "Collections"
2. Haz clic en el botón "+" o "Create a collection"
3. Nómbrala: Biblioteca API - REST
4. Agrega una descripción: Pruebas de API REST del sistema de biblioteca

16.6.4. Probar API REST - Listar Libros (GET)

1 Crear primera petición GET

Asegúrate de que el servidor Django esté corriendo:

python manage.py runserver

En Postman:
1. Dentro de tu collection "Biblioteca API - REST", haz clic en "Add request"
2. Nómbrala: GET - Listar todos los libros
3. Configura la petición:

Método: GET (ya está seleccionado por defecto)
URL: http://127.0.0.1:8000/api/libros/

4. Haz clic en el botón azul "Send"
5. Verás la respuesta JSON con todos los libros en la parte inferior

Respuesta esperada: Código de estado 200 OK y JSON con lista de libros:
{
    "count": 7,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": 1,
            "titulo": "Cien años de soledad",
            "autor_nombre": "Gabriel García Márquez",
            "stock_disponible": 3,
            ...
        }
    ]
}

16.6.5. Probar API REST - Obtener un Libro por ID (GET)

Crear petición GET con parámetro

1. Agrega otra request a la collection
2. Nombre: GET - Obtener libro por ID
3. Método: GET
4. URL: http://127.0.0.1:8000/api/libros/1/ (ID = 1)
5. Haz clic en "Send"

Resultado: Información detallada del libro con ID 1

16.6.6. Probar API REST - Buscar Libros (GET con Query Params)

Usar parámetros de búsqueda

1. Nueva request: GET - Buscar libros por título
2. Método: GET
3. URL: http://127.0.0.1:8000/api/libros/
4. Ve a la pestaña "Params"
5. Agrega parámetro:

Key: search
Value: soledad

6. Haz clic en "Send"
7. Verás solo los libros que contengan "soledad" en el título

16.6.7. Probar API REST - Crear un Libro (POST)

1 Crear petición POST con datos JSON

1. Nueva request: POST - Crear nuevo libro
2. Método: POST
3. URL: http://127.0.0.1:8000/api/libros/
4. Ve a la pestaña "Body"
5. Selecciona "raw"
6. En el dropdown de la derecha, selecciona "JSON"
7. Escribe este JSON:

{
    "titulo": "El principito",
    "isbn": "9788478887194",
    "autor": 1,
    "categoria": 1,
    "editorial": 1,
    "fecha_publicacion": "1943-04-06",
    "numero_paginas": 96,
    "idioma": "Español",
    "descripcion": "Novela corta del escritor francés Antoine de Saint-Exupéry",
    "stock_total": 5,
    "stock_disponible": 5,
    "estado": "disponible"
}

8. Haz clic en "Send"

⚠️ Si obtienes error 403 Forbidden: La API requiere autenticación. Por ahora puedes cambiar los permisos en settings.py a 'rest_framework.permissions.AllowAny' para pruebas.
Resultado esperado: Código 201 Created y el JSON del libro creado con su ID asignado.

16.6.8. Probar API REST - Actualizar un Libro (PUT)

Actualizar libro completo con PUT

1. Nueva request: PUT - Actualizar libro completo
2. Método: PUT
3. URL: http://127.0.0.1:8000/api/libros/1/ (actualizar libro ID 1)
4. Body → raw → JSON
5. JSON completo del libro con cambios (cambia solo el stock_disponible):

{
    "titulo": "Cien años de soledad",
    "isbn": "9780307474728",
    "autor": 1,
    "categoria": 1,
    "editorial": 1,
    "fecha_publicacion": "1967-05-30",
    "numero_paginas": 471,
    "idioma": "Español",
    "descripcion": "Obra maestra del realismo mágico",
    "stock_total": 5,
    "stock_disponible": 2,
    "estado": "disponible"
}

6. Send → Código 200 OK

16.6.9. Probar API REST - Actualización Parcial (PATCH)

Actualizar solo campos específicos

1. Nueva request: PATCH - Actualizar stock
2. Método: PATCH
3. URL: http://127.0.0.1:8000/api/libros/1/
4. Body → raw → JSON (solo los campos a cambiar):

{
    "stock_disponible": 4,
    "estado": "disponible"
}

5. Send → Solo esos campos se actualizan, el resto queda igual

16.6.10. Probar API REST - Eliminar un Libro (DELETE)

Eliminar libro por ID

1. Nueva request: DELETE - Eliminar libro
2. Método: DELETE
3. URL: http://127.0.0.1:8000/api/libros/8/ (elimina libro ID 8)
4. Send → Código 204 No Content (eliminado exitosamente)

⚠️ Cuidado: DELETE elimina permanentemente el registro de la base de datos.

16.6.11. Probar Otros Endpoints REST

Completar la colección con todos los endpoints

Crea requests para probar estos endpoints adicionales:

Método URL Descripción
GET /api/autores/ Listar todos los autores
GET /api/autores/1/ Obtener autor por ID
POST /api/autores/ Crear nuevo autor
GET /api/categorias/ Listar categorías
POST /api/categorias/ Crear categoría
GET /api/editoriales/ Listar editoriales
GET /api/prestamos/ Listar préstamos
POST /api/prestamos/ Crear préstamo
GET /api/libros/?categoria=1 Filtrar por categoría
GET /api/libros/?autor=1 Filtrar por autor

16.6.12. Probar Servicio SOAP con Postman

ℹ️ Postman también soporta SOAP! Aunque SOAP es más complejo, Postman puede enviar mensajes XML SOAP.
1 Crear Collection para SOAP

1. Crea una nueva collection: Biblioteca API - SOAP
2. Agrega descripción: Pruebas de servicio SOAP

2 SOAP - Listar Libros

1. Nueva request: SOAP - Listar libros
2. Método: POST
3. URL: http://127.0.0.1:8000/soap/
4. Pestaña "Headers" → Agregar:

Key: Content-Type
Value: text/xml; charset=utf-8

5. Pestaña "Body" → raw → XML:

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:bib="biblioteca.soap.services">
    <soap:Body>
        <bib:listar_libros/>
    </soap:Body>
</soap:Envelope>

6. Send → Respuesta en XML con lista de libros

3 SOAP - Obtener Libro por ID

1. Nueva request: SOAP - Obtener libro
2. POST → http://127.0.0.1:8000/soap/
3. Header: Content-Type: text/xml; charset=utf-8
4. Body → raw → XML:

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:bib="biblioteca.soap.services">
    <soap:Body>
        <bib:obtener_libro>
            <bib:libro_id>1</bib:libro_id>
        </bib:obtener_libro>
    </soap:Body>
</soap:Envelope>

5. Send → XML con detalles del libro ID 1

4 SOAP - Crear Préstamo

1. Nueva request: SOAP - Crear préstamo
2. POST → http://127.0.0.1:8000/soap/
3. Header: Content-Type: text/xml
4. Body:

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:bib="biblioteca.soap.services">
    <soap:Body>
        <bib:crear_prestamo>
            <bib:libro_id>2</bib:libro_id>
            <bib:usuario_id>1</bib:usuario_id>
            <bib:dias_prestamo>14</bib:dias_prestamo>
        </bib:crear_prestamo>
    </soap:Body>
</soap:Envelope>

5. Send → Respuesta con resultado de la operación

16.6.13. Exportar Colecciones de Postman

Guardar tu trabajo

Para guardar todas tus pruebas:

  1. Haz clic derecho en la collection "Biblioteca API - REST"
  2. Selecciona "Export"
  3. Elige "Collection v2.1"
  4. Haz clic en "Export"
  5. Guarda el archivo JSON en tu proyecto
  6. Repite para la collection "Biblioteca API - SOAP"

16.6.14. Resumen de Endpoints Probados

APIs REST Probadas:
• GET - Listar libros ✅
• GET - Obtener libro por ID ✅
• GET - Buscar libros ✅
• POST - Crear libro ✅
• PUT - Actualizar libro completo ✅
• PATCH - Actualización parcial ✅
• DELETE - Eliminar libro ✅
• GET - Autores, Categorías, Editoriales ✅
• GET/POST - Préstamos ✅

APIs SOAP Probadas:
• Listar libros ✅
• Obtener libro por ID ✅
• Crear préstamo ✅

Total de pruebas: Mínimo 12 requests diferentes
Cuenta Postman: Con apellidos del estudiante ✅
⚠️ Para el examen: Debes tener tu workspace de Postman con TODAS las pruebas realizadas y guardadas. El nombre de usuario debe contener tus apellidos.

16.7. Despliegue con Docker (Producción Avanzada)

ℹ️ Requisito: Debes tener Docker Desktop instalado (ver Sección 3.5).

16.6.1. ¿Por qué usar Docker para este proyecto?

Ventajas del despliegue con Docker
  • 🚀 Despliegue instantáneo: Todo el sistema con un solo comando
  • 🔧 Configuración automática: MySQL, Django, Nginx y Redis pre-configurados
  • 📦 Portabilidad: Funciona igual en Windows, Mac y Linux
  • ⚖️ Escalabilidad: Fácil escalar múltiples instancias
  • 🔒 Aislamiento: No afecta tu sistema ni otras aplicaciones
  • 🌐 Producción: Simula un entorno de producción real

16.6.2. Crear archivos de configuración Docker

ℹ️ Importante: Debes crear estos archivos de configuración en la raíz de tu proyecto (django-sistema-u3/) para poder usar Docker.
1 Crear archivo Dockerfile

En la raíz del proyecto (django-sistema-u3/), crea un archivo llamado Dockerfile (sin extensión) y agrega el siguiente contenido:

Dockerfile
# Dockerfile para Sistema de Biblioteca
# Basado en Python 3.11 (compatible con Spyne)

FROM python:3.11-slim

# Variables de entorno
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# Información del mantenedor
LABEL maintainer="Sistema de Biblioteca U3"
LABEL description="Sistema de gestión de biblioteca con SOAP y REST API"

# Crear directorio de trabajo
WORKDIR /app

# Instalar dependencias del sistema necesarias para lxml y otras bibliotecas
RUN apt-get update && apt-get install -y \
    gcc \
    libxml2-dev \
    libxslt-dev \
    python3-dev \
    default-libmysqlclient-dev \
    pkg-config \
    && rm -rf /var/lib/apt/lists/*

# Copiar requirements primero (para aprovechar cache de Docker)
COPY requirements.txt .

# Instalar dependencias Python
RUN pip install --upgrade pip && \
    pip install -r requirements.txt

# Copiar el resto del código
COPY . .

# Crear directorio para archivos estáticos
RUN mkdir -p /app/staticfiles

# Recolectar archivos estáticos
RUN python manage.py collectstatic --noinput || true

# Exponer puerto
EXPOSE 8000

# Script de inicio que ejecuta migraciones y inicia el servidor
CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"]
2 Crear archivo requirements.txt

Si no existe, crea el archivo requirements.txt en la raíz del proyecto:

requirements.txt
asgiref==3.11.0
attrs==25.4.0
certifi==2026.1.4
charset-normalizer==3.4.4
Django==5.2.10
django-filter==25.2
djangorestframework==3.14.0
idna==3.11
isodate==0.7.2
lxml==6.0.2
platformdirs==4.5.1
pytz==2025.2
requests==2.31.0
requests-file==3.0.1
requests-toolbelt==1.0.0
spyne==2.14.0
sqlparse==0.5.5
tzdata==2025.3
urllib3==2.6.3
zeep==4.2.1
pymysql==1.1.1
mysql-connector-python==9.5.0
💡 Tip: También puedes generar este archivo automáticamente con: pip freeze > requirements.txt (con el entorno virtual activado)
3 Crear archivo docker-compose.yml

Crea el archivo docker-compose.yml en la raíz del proyecto:

docker-compose.yml
# docker-compose.yml
# Orquestación multi-contenedor para Sistema de Biblioteca

version: '3.8'

services:
  # Base de datos MySQL
  db:
    image: mysql:8.0
    container_name: biblioteca_mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_password_123
      MYSQL_DATABASE: biblioteca_db
      MYSQL_USER: biblioteca_user
      MYSQL_PASSWORD: biblioteca_pass_123
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - biblioteca_network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot_password_123"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Aplicación Django
  web:
    build: .
    container_name: biblioteca_web
    restart: always
    command: sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
    volumes:
      - .:/app
      - static_volume:/app/staticfiles
    ports:
      - "8000:8000"
    environment:
      - DJANGO_SETTINGS_MODULE=biblioteca_project.settings
      - PYTHONUNBUFFERED=1
    depends_on:
      - db
    networks:
      - biblioteca_network

  # Nginx como balanceador de carga y servidor de archivos estáticos
  nginx:
    image: nginx:alpine
    container_name: biblioteca_nginx
    restart: always
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - static_volume:/usr/share/nginx/html/static:ro
    depends_on:
      - web
    networks:
      - biblioteca_network

  # Servicio adicional: Redis para caché (opcional pero útil)
  redis:
    image: redis:7-alpine
    container_name: biblioteca_redis
    restart: always
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - biblioteca_network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3

# Volúmenes persistentes
volumes:
  mysql_data:
    driver: local
  static_volume:
    driver: local
  redis_data:
    driver: local

# Red personalizada
networks:
  biblioteca_network:
    driver: bridge
4 Crear archivo nginx.conf

Crea el archivo nginx.conf en la raíz del proyecto:

nginx.conf
# Configuración de Nginx como balanceador de carga y proxy reverso

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # Compresión
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;

    # Upstream para balanceo de carga
    upstream django_backend {
        least_conn;
        server web:8000 weight=1 max_fails=3 fail_timeout=30s;
    }

    # Servidor principal
    server {
        listen 80;
        server_name localhost;
        client_max_body_size 20M;

        # Health check endpoint
        location /health {
            access_log off;
            return 200 "healthy\n";
            add_header Content-Type text/plain;
        }

        # Archivos estáticos
        location /static/ {
            alias /usr/share/nginx/html/static/;
            expires 30d;
            add_header Cache-Control "public, immutable";
        }

        # Admin de Django
        location /admin/ {
            proxy_pass http://django_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
        }

        # API REST
        location /api/ {
            proxy_pass http://django_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
        }

        # Servicio SOAP
        location /soap/ {
            proxy_pass http://django_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
        }

        # Todas las demás solicitudes
        location / {
            proxy_pass http://django_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
        }

        # Página de error
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root /usr/share/nginx/html;
        }
    }
}
5 Crear archivo docker-compose.scale.yml (Opcional)

Para escalamiento avanzado, crea docker-compose.scale.yml:

docker-compose.scale.yml
# docker-compose.scale.yml
# Configuración para escalar múltiples instancias de Django

version: '3.8'

services:
  db:
    image: mysql:8.0
    container_name: biblioteca_mysql
    restart: always
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: root_password_123
      MYSQL_DATABASE: biblioteca_db
      MYSQL_USER: biblioteca_user
      MYSQL_PASSWORD: biblioteca_pass_123
    ports:
      - "3307:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - biblioteca_network

  web:
    build: .
    restart: always
    command: sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
    volumes:
      - .:/app
      - static_volume:/app/staticfiles
    environment:
      - DJANGO_SETTINGS_MODULE=biblioteca_project.settings
      - PYTHONUNBUFFERED=1
    depends_on:
      - db
      - redis
    networks:
      - biblioteca_network
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

  nginx:
    image: nginx:alpine
    container_name: biblioteca_nginx_lb
    restart: always
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - static_volume:/usr/share/nginx/html/static:ro
    depends_on:
      - web
    networks:
      - biblioteca_network

  redis:
    image: redis:7-alpine
    container_name: biblioteca_redis
    restart: always
    command: redis-server --appendonly yes
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - biblioteca_network

volumes:
  mysql_data:
  static_volume:
  redis_data:

networks:
  biblioteca_network:
    driver: bridge
¡Archivos Docker creados! Ahora tienes todos los archivos de configuración necesarios para desplegar con Docker. Continúa con la siguiente sección para levantar los contenedores.

16.6.3. Levantar el sistema con Docker

1 Construir y levantar los contenedores
# Ir a la carpeta del proyecto cd django-sistema-u3
# Construir las imágenes y levantar servicios docker-compose up --build -d

Esto iniciará:
• MySQL en el puerto 3306
• Django en el puerto 8000
• Nginx en el puerto 80
• Redis en el puerto 6379

2 Verificar que los contenedores estén corriendo
docker-compose ps

Deberías ver 4 contenedores en estado "Up".

3 Acceder al sistema

Ahora puedes acceder al sistema en:
Nginx (producción): http://localhost
Django directo: http://localhost:8000
Admin: http://localhost/admin/

16.6.4. Comandos útiles de Docker Compose

Comando Descripción
docker-compose up -d Iniciar servicios en modo desacoplado
docker-compose down Detener y eliminar contenedores
docker-compose ps Ver estado de los servicios
docker-compose logs -f Ver logs en tiempo real
docker-compose logs web Ver logs solo del servicio Django
docker-compose restart Reiniciar todos los servicios
docker-compose exec web bash Entrar al contenedor de Django
docker-compose exec db mysql -u root -p Acceder a MySQL

16.6.5. Escalar la aplicación

Levantar múltiples instancias de Django
# Escalar a 3 instancias de Django docker-compose -f docker-compose.scale.yml up --scale web=3 -d

Nginx balanceará la carga entre las 3 instancias automáticamente.

⚠️ Nota: El despliegue con Docker es obligatorio. Para el examen, es suficiente con el despliegue local usando MySQL y el servidor de desarrollo de Django.
¡Sistema desplegado con Docker! Ahora tienes un entorno de producción completo con balanceo de carga, caché y bases de datos aisladas.

17. Solución de Problemas Comunes

17.1. Error: "No module named pymysql"

⚠️ Problema: No se instaló pymysql o el entorno virtual no está activado.

Solución:

# Activar entorno virtual venv311\Scripts\activate
# Instalar pymysql pip install pymysql mysqlclient

17.2. Error: "Access denied for user 'root'@'localhost'"

⚠️ Problema: Contraseña de MySQL incorrecta.

Solución:

Verifica la contraseña en biblioteca_project/settings.py en la sección DATABASES. Debe coincidir con la contraseña que configuraste en MySQL.

17.3. Error: "Unknown database 'biblioteca_dbutres'"

⚠️ Problema: La base de datos no existe.

Solución:

# Abrir MySQL mysql -u root -p
# Crear base de datos CREATE DATABASE biblioteca_dbutres CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; exit;

17.4. Error: "Page not found (404)"

⚠️ Problema: Las URLs no están configuradas correctamente.

Solución:

Verifica que:
1. Creaste el archivo biblioteca_project/views.py
2. Configuraste correctamente biblioteca_project/urls.py
3. Los templates existen en biblioteca_project/templates/

17.5. Error: "Invalid datetime value"

⚠️ Problema: Problema de zona horaria con MySQL.

Solución:

En settings.py, asegúrate de tener:

USE_TZ = False
TIME_ZONE = 'America/Hermosillo'

17.6. El CSS/JS no se carga

⚠️ Problema: Archivos estáticos no configurados.

Solución:

python manage.py collectstatic --noinput

Verifica que en settings.py tengas configurado STATIC_URL y STATICFILES_DIRS.

17.7. Error en cliente SOAP: "ConnectionError"

⚠️ Problema: El servidor Django no está corriendo.

Solución:

1. Asegúrate de que el servidor Django esté corriendo en otra terminal:
python manage.py runserver

2. Verifica que puedas acceder al WSDL en el navegador:
http://127.0.0.1:8000/soap/?wsdl

3. Si cambias el puerto del servidor, actualiza el WSDL_URL en cliente_soap_visual.py

17.8. Error: "No module named 'zeep'"

⚠️ Problema: La librería zeep no está instalada.

Solución:

# Activar entorno virtual venv311\Scripts\activate
# Instalar zeep pip install zeep

17.9. Docker: "Cannot connect to Docker daemon"

⚠️ Problema: Docker Desktop no está corriendo.

Solución:

1. Abre Docker Desktop desde el menú de inicio
2. Espera a que se inicie completamente (verás el ícono de ballena verde)
3. Intenta nuevamente el comando docker

17.10. Docker: Error de permisos en Windows

⚠️ Problema: Falta WSL 2 o permisos de administrador.

Solución:

1. Ejecuta PowerShell como Administrador
2. Instala WSL 2:
wsl --install
3. Reinicia tu computadora
4. Abre Docker Desktop y verifica que esté usando WSL 2

17.11. Error SOAP: "Libro no encontrado"

⚠️ Problema: ID inválido o base de datos vacía.

Solución:

1. Verifica que la base de datos tenga datos:
python populate_db.py

2. Usa la opción 1 del cliente SOAP para listar libros y obtener IDs válidos

3. Verifica en el admin de Django que existan libros:
http://127.0.0.1:8000/admin/libros/libro/

17.12. El servicio SOAP responde muy lento

⚠️ Problema: Muchas consultas o falta de índices en BD.

Solución:

1. Optimiza las consultas usando select_related() y prefetch_related()
2. Agrega índices a los campos más consultados en models.py
3. Considera usar Redis para cachear respuestas frecuentes

18. Código Completo de Archivos Auxiliares

ℹ️ En esta sección: Código completo de archivos auxiliares listos para copiar y pegar en las ubicaciones exactas indicadas.

🎯 ¿Qué hacen estos 3 archivos y qué resultado obtendrás?

📋 1. cliente_soap_visual.py - Cliente SOAP Interactivo

¿Qué hace? Es un programa Python que te permite probar tu servicio SOAP de manera interactiva desde la terminal.

Características principales:

  • ✅ Menú interactivo con 3 operaciones principales (listar libros, obtener libro, crear préstamo)
  • ✅ Captura automáticamente los mensajes SOAP Request y Response
  • ✅ Genera un archivo HTML visual con el XML formateado
  • ✅ Abre automáticamente el navegador para mostrar los mensajes SOAP

Resultado: Después de cada operación, se abre una página web bonita que muestra el XML Request y Response, con botones para copiar y estadísticas.

🔧 2. fix_spyne_cgi.py - Parche para Python 3.13+

¿Qué hace? Soluciona un problema crítico de compatibilidad entre Spyne y Python 3.13+ (el módulo cgi fue eliminado).

¿Por qué lo necesitas?

  • ⚠️ Sin este script, Spyne NO funciona en Python 3.13+
  • ✅ Parchea automáticamente los archivos de Spyne
  • ✅ Reemplaza import cgi con email.message
  • ✅ Solo necesitas ejecutarlo UNA vez antes de iniciar el servidor

Resultado: Tu servicio SOAP funcionará perfectamente sin errores de módulo cgi no encontrado.

📄 3. EJEMPLO_VISUALIZADOR_SOAP.html - Vista previa del resultado

¿Qué es? Un archivo HTML de ejemplo que muestra exactamente cómo se verá la página que genera cliente_soap_visual.py.

Contenido del ejemplo:

  • 📤 Mensaje SOAP Request (solicitud) de ejemplo
  • 📥 Mensaje SOAP Response (respuesta) de ejemplo
  • 📊 Estadísticas (tamaño en bytes, protocolo, formato)
  • 📋 Botones funcionales para copiar el XML
  • 🎨 Diseño profesional con gradientes y colores

Resultado: Puedes abrirlo ahora mismo en tu navegador para ver cómo se verán tus mensajes SOAP después de ejecutar el cliente.

🎬 Flujo completo de trabajo:

1. Ejecutas: python fix_spyne_cgi.py

→ Parchea Spyne (solo una vez)

2. Inicias el servidor: python manage.py runserver

→ Django + SOAP corriendo en http://127.0.0.1:8000

3. Ejecutas el cliente: python cliente_soap_visual.py

→ Menú interactivo aparece en la terminal

4. Seleccionas una opción (ej: opción 1 - Listar libros)

→ El cliente envía la petición SOAP al servidor

5. Te pregunta: "¿Ver XML en navegador? (s/n)"

→ Respondes "s"

6. 🌐 Se abre automáticamente tu navegador

→ Página HTML hermosa (como EJEMPLO_VISUALIZADOR_SOAP.html)

→ Muestra el XML Request y Response de tu operación

→ Puedes copiar el XML con un clic

💡 Tip: El archivo EJEMPLO_VISUALIZADOR_SOAP.html es solo para que veas cómo se verá el resultado. El cliente Python genera archivos HTML similares automáticamente cada vez que ejecutas una operación.

18.1. cliente_soap_visual.py

📍 Ubicación: Raíz del proyecto

Ruta completa: django-sistema-u3/cliente_soap_visual.py

💡 ¿Qué hace? Cliente interactivo en Python para probar todas las operaciones del servicio SOAP desde la terminal.
cliente_soap_visual.py - CÓDIGO COMPLETO (814 líneas)
#!/usr/bin/env python3
"""
Cliente SOAP Visual - Sistema de Biblioteca
Ejecuta operaciones y muestra XML en navegador
"""

from zeep import Client, Settings
from zeep.exceptions import Fault
from zeep.plugins import HistoryPlugin
import sys
import webbrowser
import os
import tempfile
from datetime import datetime
from lxml import etree

WSDL_URL = 'http://127.0.0.1:8000/soap/?wsdl'

# Plugin para capturar mensajes SOAP
history = HistoryPlugin()

def crear_cliente():
    """Crea cliente SOAP con configuración optimizada"""
    try:
        settings = Settings(strict=False, xml_huge_tree=True, xsd_ignore_sequence_order=True)
        client = Client(WSDL_URL, settings=settings, plugins=[history])
        return client
    except Exception as e:
        print(f"❌ Error al conectar con el servidor SOAP: {e}")
        print("\n💡 Asegúrate de que el servidor Django esté corriendo:")
        print("   python manage.py runserver")
        sys.exit(1)

def formatear_xml(xml_string):
    """Formatea XML para mejor visualización"""
    try:
        parser = etree.XMLParser(remove_blank_text=True)
        root = etree.fromstring(xml_string, parser)
        return etree.tostring(root, pretty_print=True, encoding='unicode')
    except:
        return xml_string

def mostrar_xml_en_navegador(request_xml, response_xml, operacion):
    """Genera HTML con XML SOAP y lo abre en navegador"""
    import html
    
    request_formatted = formatear_xml(request_xml)
    response_formatted = formatear_xml(response_xml)
    
    request_escaped = html.escape(request_formatted)
    response_escaped = html.escape(response_formatted)
    
    html_content = f"""
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>🔍 XML SOAP - {operacion}</title>
    <style>
        * {{ margin: 0; padding: 0; box-sizing: border-box; }}
        body {{ font-family: 'Segoe UI', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; min-height: 100vh; }}
        .container {{ max-width: 1600px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden; }}
        .header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px 40px; text-align: center; }}
        .header h1 {{ font-size: 2.5em; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.2); }}
        .header .info {{ font-size: 1.1em; opacity: 0.95; margin: 5px 0; }}
        .content {{ padding: 40px; }}
        .section {{ margin-bottom: 40px; }}
        .section-title {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 25px; border-radius: 10px; font-size: 1.5em; margin-bottom: 20px; display: flex; align-items: center; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); }}
        .section-title .icon {{ margin-right: 15px; font-size: 1.5em; }}
        .xml-container {{ background: #1e1e1e; border-radius: 10px; padding: 25px; overflow-x: auto; box-shadow: inset 0 2px 10px rgba(0,0,0,0.3); max-height: 600px; overflow-y: auto; }}
        pre {{ margin: 0; color: #d4d4d4; font-family: 'Consolas', 'Monaco', monospace; font-size: 0.95em; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; }}
        .copy-btn {{ background: #4CAF50; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 1em; margin-top: 15px; transition: all 0.3s; box-shadow: 0 4px 10px rgba(76, 175, 80, 0.3); }}
        .copy-btn:hover {{ background: #45a049; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(76, 175, 80, 0.4); }}
        .stats {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }}
        .stat-card {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px; text-align: center; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); }}
        .stat-card .value {{ font-size: 2em; font-weight: bold; margin-bottom: 5px; }}
        .stat-card .label {{ font-size: 0.9em; opacity: 0.9; }}
        .footer {{ text-align: center; padding: 20px; background: #f5f5f5; color: #666; font-size: 0.9em; }}
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🔍 Visualizador de XML SOAP</h1>
            <div class="info"><strong>Operación:</strong> {operacion}</div>
            <div class="info"><strong>Timestamp:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</div>
            <div class="info"><strong>Servidor:</strong> {WSDL_URL}</div>
        </div>
        <div class="content">
            <div class="stats">
                <div class="stat-card"><div class="value">{len(request_xml)}</div><div class="label">Bytes Request</div></div>
                <div class="stat-card"><div class="value">{len(response_xml)}</div><div class="label">Bytes Response</div></div>
                <div class="stat-card"><div class="value">{request_formatted.count('<')}</div><div class="label">Tags Request</div></div>
                <div class="stat-card"><div class="value">{response_formatted.count('<')}</div><div class="label">Tags Response</div></div>
            </div>
            <div class="section">
                <div class="section-title"><span class="icon">📤</span><span>SOAP Request</span></div>
                <div class="xml-container"><pre id="request-xml">{request_escaped}</pre></div>
                <button class="copy-btn" onclick="copyToClipboard('request-xml')">📋 Copiar Request</button>
            </div>
            <div class="section">
                <div class="section-title"><span class="icon">📥</span><span>SOAP Response</span></div>
                <div class="xml-container"><pre id="response-xml">{response_escaped}</pre></div>
                <button class="copy-btn" onclick="copyToClipboard('response-xml')">📋 Copiar Response</button>
            </div>
        </div>
        <div class="footer">Sistema de Biblioteca - Cliente SOAP Visual<br>Universidad Tecnológica de Hermosillo</div>
    </div>
    <script>
        function copyToClipboard(elementId) {{
            const text = document.getElementById(elementId).textContent;
            navigator.clipboard.writeText(text).then(() => {{
                const btn = event.target;
                const originalText = btn.textContent;
                btn.textContent = '✅ Copiado!';
                btn.style.background = '#2196F3';
                setTimeout(() => {{ btn.textContent = originalText; btn.style.background = '#4CAF50'; }}, 2000);
            }}).catch(err => {{ alert('Error al copiar: ' + err); }});
        }}
    </script>
</body>
</html>
"""
    
    # Guardar HTML en archivo temporal y abrirlo
    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.html', encoding='utf-8') as f:
        f.write(html_content)
        temp_file = f.name
    
    print(f"\n🌐 Abriendo visualizador XML en el navegador...")
    webbrowser.open('file://' + os.path.abspath(temp_file))

def preguntar_ver_xml():
    """Pregunta si desea ver el XML"""
    print("\n" + "="*80)
    respuesta = input("¿Desea ver el XML SOAP en el navegador? (s/n): ").strip().lower()
    return respuesta in ['s', 'si', 'sí', 'y', 'yes']

# ===== FUNCIONES DE OPERACIONES =====

def listar_libros(client):
    """Lista todos los libros"""
    print("\n" + "="*80)
    print("📚 LISTANDO TODOS LOS LIBROS")
    print("="*80)
    
    try:
        result = client.service.listar_libros()
        
        if not result:
            print("\n⚠️  No hay libros registrados en la biblioteca")
            return
        
        print(f"\n✅ Se encontraron {len(result)} libros:\n")
        
        for i, libro in enumerate(result, 1):
            print(f"\n{i}. 📖 {libro.titulo}")
            print(f"   ID: {libro.id}")
            print(f"   📝 ISBN: {libro.isbn}")
            print(f"   ✍️  Autor: {libro.autor_nombre}")
            print(f"   📚 Editorial: {libro.editorial_nombre}")
            print(f"   🏷️  Categoría: {libro.categoria_nombre}")
            print(f"   📅 Publicación: {libro.fecha_publicacion}")
            print(f"   📄 Páginas: {libro.numero_paginas}")
            print(f"   🌐 Idioma: {libro.idioma}")
            print(f"   📊 Estado: {libro.estado}")
            print(f"   📦 Disponibles: {libro.stock_disponible}")
            if libro.ubicacion_fisica:
                print(f"   📍 Ubicación: {libro.ubicacion_fisica}")
            if libro.descripcion:
                print(f"   📖 Descripción: {libro.descripcion[:100]}...")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, "listar_libros")
            
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def obtener_libro(client):
    """Obtiene detalles de un libro por ID"""
    print("\n" + "="*80)
    print("🔍 OBTENER LIBRO POR ID")
    print("="*80)
    
    try:
        libro_id = int(input("\nIngrese el ID del libro: "))
        result = client.service.obtener_libro(libro_id)
        
        if not result:
            print(f"\n⚠️  No se encontró libro con ID {libro_id}")
            return
        
        print(f"\n✅ Libro encontrado:\n")
        print(f"📖 {result.titulo}")
        print(f"ID: {result.id}")
        print(f"📝 ISBN: {result.isbn}")
        print(f"\n👤 AUTOR:")
        print(f"   Nombre: {result.autor.nombre} {result.autor.apellido}")
        print(f"   Nacionalidad: {result.autor.nacionalidad}")
        print(f"   Biografía: {result.autor.biografia[:100] if result.autor.biografia else 'N/A'}...")
        print(f"\n🏢 EDITORIAL:")
        print(f"   Nombre: {result.editorial.nombre}")
        print(f"   País: {result.editorial.pais}")
        print(f"   Web: {result.editorial.sitio_web}")
        print(f"\n🏷️  CATEGORÍA:")
        print(f"   Nombre: {result.categoria.nombre}")
        print(f"   Descripción: {result.categoria.descripcion}")
        print(f"\n📄 DETALLES:")
        print(f"   Páginas: {result.numero_paginas}")
        print(f"   Idioma: {result.idioma}")
        print(f"   Publicación: {result.fecha_publicacion}")
        print(f"   Registro: {result.fecha_registro}")
        print(f"   Estado: {result.estado}")
        print(f"   Stock Total: {result.stock_total}")
        print(f"   Stock Disponible: {result.stock_disponible}")
        print(f"   Ubicación: {result.ubicacion_fisica}")
        print(f"\n📖 Descripción:")
        print(f"   {result.descripcion}")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, f"obtener_libro (ID: {libro_id})")
            
    except ValueError:
        print("\n❌ Error: Debe ingresar un número válido")
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def buscar_libros_por_titulo(client):
    """Busca libros por título"""
    print("\n" + "="*80)
    print("🔍 BUSCAR LIBROS POR TÍTULO")
    print("="*80)
    
    titulo = input("\nIngrese texto a buscar en el título: ").strip()
    
    if not titulo:
        print("\n❌ Debe ingresar un texto para buscar")
        return
    
    try:
        result = client.service.buscar_libros_por_titulo(titulo)
        
        if not result:
            print(f"\n⚠️  No se encontraron libros con '{titulo}' en el título")
            return
        
        print(f"\n✅ Se encontraron {len(result)} libros:\n")
        
        for i, libro in enumerate(result, 1):
            print(f"{i}. 📖 {libro.titulo}")
            print(f"   Autor: {libro.autor_nombre} | Categoría: {libro.categoria_nombre}")
            print(f"   Estado: {libro.estado} | Disponibles: {libro.stock_disponible}")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, f"buscar_libros_por_titulo ('{titulo}')")
            
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def buscar_libros_por_autor(client):
    """Busca libros por apellido del autor"""
    print("\n" + "="*80)
    print("🔍 BUSCAR LIBROS POR AUTOR")
    print("="*80)
    
    apellido = input("\nIngrese el apellido del autor: ").strip()
    
    if not apellido:
        print("\n❌ Debe ingresar un apellido")
        return
    
    try:
        result = client.service.buscar_libros_por_autor(apellido)
        
        if not result:
            print(f"\n⚠️  No se encontraron libros del autor '{apellido}'")
            return
        
        print(f"\n✅ Se encontraron {len(result)} libros:\n")
        
        for i, libro in enumerate(result, 1):
            print(f"{i}. 📖 {libro.titulo}")
            print(f"   Autor: {libro.autor_nombre}")
            print(f"   Categoría: {libro.categoria_nombre} | Año: {libro.fecha_publicacion}")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, f"buscar_libros_por_autor ('{apellido}')")
            
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def buscar_libros_por_categoria(client):
    """Busca libros por categoría"""
    print("\n" + "="*80)
    print("🔍 BUSCAR LIBROS POR CATEGORÍA")
    print("="*80)
    
    categoria = input("\nIngrese el nombre de la categoría: ").strip()
    
    if not categoria:
        print("\n❌ Debe ingresar una categoría")
        return
    
    try:
        result = client.service.buscar_libros_por_categoria(categoria)
        
        if not result:
            print(f"\n⚠️  No se encontraron libros en la categoría '{categoria}'")
            return
        
        print(f"\n✅ Se encontraron {len(result)} libros:\n")
        
        for i, libro in enumerate(result, 1):
            print(f"{i}. 📖 {libro.titulo}")
            print(f"   Autor: {libro.autor_nombre}")
            print(f"   Disponibles: {libro.stock_disponible} | Estado: {libro.estado}")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, f"buscar_libros_por_categoria ('{categoria}')")
            
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def listar_libros_disponibles(client):
    """Lista libros disponibles"""
    print("\n" + "="*80)
    print("📚 LIBROS DISPONIBLES PARA PRÉSTAMO")
    print("="*80)
    
    try:
        result = client.service.listar_libros_disponibles()
        
        if not result:
            print("\n⚠️  No hay libros disponibles actualmente")
            return
        
        print(f"\n✅ Se encontraron {len(result)} libros disponibles:\n")
        
        for i, libro in enumerate(result, 1):
            print(f"{i}. 📖 {libro.titulo}")
            print(f"   ID: {libro.id} | Autor: {libro.autor_nombre}")
            print(f"   Disponibles: {libro.stock_disponible} unidades")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, "listar_libros_disponibles")
            
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def crear_prestamo(client):
    """Crea un nuevo préstamo"""
    print("\n" + "="*80)
    print("➕ CREAR NUEVO PRÉSTAMO")
    print("="*80)
    
    try:
        libro_id = int(input("\nID del libro: "))
        usuario_id = int(input("ID del usuario: "))
        dias = int(input("Días de préstamo (ej: 14): "))
        
        result = client.service.crear_prestamo(libro_id, usuario_id, dias)
        
        if result.exito:
            print(f"\n✅ {result.mensaje}")
            print(f"   ID del préstamo: {result.id}")
        else:
            print(f"\n❌ {result.mensaje}")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, "crear_prestamo")
            
    except ValueError:
        print("\n❌ Error: Debe ingresar números válidos")
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def devolver_libro(client):
    """Registra devolución de libro"""
    print("\n" + "="*80)
    print("📥 DEVOLVER LIBRO")
    print("="*80)
    
    try:
        prestamo_id = int(input("\nID del préstamo: "))
        
        result = client.service.devolver_libro(prestamo_id)
        
        if result.exito:
            print(f"\n✅ {result.mensaje}")
        else:
            print(f"\n❌ {result.mensaje}")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, "devolver_libro")
            
    except ValueError:
        print("\n❌ Error: Debe ingresar un número válido")
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def ver_prestamos_usuario(client):
    """Ver préstamos de un usuario"""
    print("\n" + "="*80)
    print("👤 PRÉSTAMOS DE USUARIO")
    print("="*80)
    
    try:
        usuario_id = int(input("\nID del usuario: "))
        
        result = client.service.obtener_prestamos_usuario(usuario_id)
        
        if not result:
            print(f"\n⚠️  El usuario no tiene préstamos registrados")
            return
        
        print(f"\n✅ Se encontraron {len(result)} préstamos:\n")
        
        for i, prestamo in enumerate(result, 1):
            print(f"{i}. 📖 {prestamo.libro_titulo}")
            print(f"   ID: {prestamo.id} | Usuario: {prestamo.usuario_nombre}")
            print(f"   Préstamo: {prestamo.fecha_prestamo}")
            print(f"   Devolución esperada: {prestamo.fecha_devolucion_esperada}")
            if prestamo.fecha_devolucion_real:
                print(f"   Devuelto: {prestamo.fecha_devolucion_real}")
            print(f"   Estado: {prestamo.estado}")
            if prestamo.multa and float(prestamo.multa) > 0:
                print(f"   💰 Multa: ${prestamo.multa}")
            print()
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, f"obtener_prestamos_usuario (ID: {usuario_id})")
            
    except ValueError:
        print("\n❌ Error: Debe ingresar un número válido")
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def listar_prestamos_activos(client):
    """Lista préstamos activos"""
    print("\n" + "="*80)
    print("📋 PRÉSTAMOS ACTIVOS")
    print("="*80)
    
    try:
        result = client.service.listar_prestamos_activos()
        
        if not result:
            print("\n⚠️  No hay préstamos activos")
            return
        
        print(f"\n✅ Se encontraron {len(result)} préstamos activos:\n")
        
        for i, prestamo in enumerate(result, 1):
            print(f"{i}. 📖 {prestamo.libro_titulo}")
            print(f"   Usuario: {prestamo.usuario_nombre}")
            print(f"   Debe devolver: {prestamo.fecha_devolucion_esperada}")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, "listar_prestamos_activos")
            
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def listar_autores(client):
    """Lista todos los autores"""
    print("\n" + "="*80)
    print("👥 LISTADO DE AUTORES")
    print("="*80)
    
    try:
        result = client.service.listar_autores()
        
        if not result:
            print("\n⚠️  No hay autores registrados")
            return
        
        print(f"\n✅ Se encontraron {len(result)} autores:\n")
        
        for i, autor in enumerate(result, 1):
            print(f"{i}. {autor.nombre} {autor.apellido}")
            print(f"   ID: {autor.id} | Nacionalidad: {autor.nacionalidad}")
            if autor.biografia:
                print(f"   Bio: {autor.biografia[:80]}...")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, "listar_autores")
            
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def listar_categorias(client):
    """Lista todas las categorías"""
    print("\n" + "="*80)
    print("🏷️  LISTADO DE CATEGORÍAS")
    print("="*80)
    
    try:
        result = client.service.listar_categorias()
        
        if not result:
            print("\n⚠️  No hay categorías registradas")
            return
        
        print(f"\n✅ Se encontraron {len(result)} categorías:\n")
        
        for i, cat in enumerate(result, 1):
            print(f"{i}. {cat.nombre}")
            print(f"   ID: {cat.id}")
            if cat.descripcion:
                print(f"   Descripción: {cat.descripcion}")
        
        if preguntar_ver_xml():
            request_xml = etree.tostring(history.last_sent['envelope'], encoding='unicode', pretty_print=True)
            response_xml = etree.tostring(history.last_received['envelope'], encoding='unicode', pretty_print=True)
            mostrar_xml_en_navegador(request_xml, response_xml, "listar_categorias")
            
    except Fault as e:
        print(f"\n❌ Error SOAP: {e}")
    except Exception as e:
        print(f"\n❌ Error: {e}")

def mostrar_menu():
    """Muestra el menú principal"""
    print("\n" + "="*80)
    print("🏛️  SISTEMA DE BIBLIOTECA - CLIENTE SOAP INTERACTIVO")
    print("="*80)
    print("\n📚 OPERACIONES DE LIBROS:")
    print("  1. Listar todos los libros")
    print("  2. Obtener libro por ID")
    print("  3. Buscar libros por título")
    print("  4. Buscar libros por autor (apellido)")
    print("  5. Buscar libros por categoría")
    print("  6. Listar libros disponibles")
    print("\n📋 OPERACIONES DE PRÉSTAMOS:")
    print("  7. Crear préstamo")
    print("  8. Devolver libro")
    print("  9. Ver préstamos de un usuario")
    print(" 10. Listar préstamos activos")
    print("\n👥 OPERACIONES DE CATÁLOGOS:")
    print(" 11. Listar autores")
    print(" 12. Listar categorías")
    print("\n  0. Salir")
    print("="*80)

def main():
    """Función principal"""
    print("\n🔌 Conectando al servidor SOAP...")
    client = crear_cliente()
    print("✅ Conexión establecida")
    
    operaciones = {
        '1': listar_libros,
        '2': obtener_libro,
        '3': buscar_libros_por_titulo,
        '4': buscar_libros_por_autor,
        '5': buscar_libros_por_categoria,
        '6': listar_libros_disponibles,
        '7': crear_prestamo,
        '8': devolver_libro,
        '9': ver_prestamos_usuario,
        '10': listar_prestamos_activos,
        '11': listar_autores,
        '12': listar_categorias,
    }
    
    while True:
        mostrar_menu()
        opcion = input("\nSeleccione una opción: ").strip()
        
        if opcion == '0':
            print("\n👋 ¡Hasta luego!")
            break
        
        if opcion in operaciones:
            operaciones[opcion](client)
        else:
            print("\n❌ Opción inválida")
        
        input("\n⏎ Presione Enter para continuar...")

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print("\n\n👋 ¡Hasta luego!")
        sys.exit(0)
⚠️ Importante: Después de copiar este código, instala las dependencias:
pip install zeep lxml
💡 ¿Cómo usar el cliente?

1. Asegúrate de que el servidor Django esté corriendo:
python manage.py runserver

2. En otra terminal, ejecuta el cliente:
python cliente_soap_visual.py

3. Usa el menú interactivo:
• Selecciona una operación (1-12)
• Ingresa los datos solicitados
• Elige si deseas ver el XML en el navegador (s/n)
• El navegador se abrirá automáticamente mostrando Request/Response
Visualización automática:
Después de cada operación, el cliente pregunta si deseas ver el XML.
Si respondes "s", se abre una página HTML hermosa en tu navegador
mostrando el Request y Response SOAP con opciones para copiar el XML.
⚠️ Importante: Después de crear el archivo, instala las dependencias necesarias:
pip install zeep lxml o si ya instalaste zeep, solo instala pip install lxml

Nota: Este cliente SOAP visual permite ver los mensajes XML Request/Response en el navegador después de cada operación, facilitando el aprendizaje de SOAP.

18.1.1. fix_spyne_cgi.py

📍 Ubicación: Raíz del proyecto

Ruta completa: django-sistema-u3/fix_spyne_cgi.py

💡 ¿Qué hace? Script para corregir el problema del módulo cgi eliminado en Python 3.13+. Este script parchea Spyne para usar email.message en su lugar, permitiendo que el servicio SOAP funcione correctamente.
fix_spyne_cgi.py
"""
Script para arreglar el problema del módulo cgi en Spyne para Python 3.13+
El módulo cgi fue eliminado en Python 3.13, este script lo reemplaza con email.message
"""
import os
import sys

def find_spyne_path():
    """Encuentra la ruta del paquete spyne"""
    try:
        import spyne
        return os.path.dirname(spyne.__file__)
    except ImportError:
        print("❌ Error: Spyne no está instalado")
        sys.exit(1)

def patch_wsgi_file(spyne_path):
    """Parchea el archivo wsgi.py que usa cgi"""
    wsgi_file = os.path.join(spyne_path, 'server', 'wsgi.py')
    
    if not os.path.exists(wsgi_file):
        print(f"❌ No se encontró: {wsgi_file}")
        return False
    
    print(f"📝 Parcheando: {wsgi_file}")
    
    with open(wsgi_file, 'r', encoding='utf-8') as f:
        content = f.read()
    
    # Reemplazar import cgi
    if 'import cgi' in content:
        # Agregar el import de email.message al inicio si no existe
        if 'from email.message import Message' not in content:
            content = content.replace(
                'import cgi',
                'from email.message import Message'
            )
        
        # Reemplazar cgi.parse_header con parse_header_value
        content = content.replace(
            'cgi.parse_header(',
            'parse_header_value('
        )
        
        # Agregar la función parse_header_value si no existe
        if 'def parse_header_value' not in content:
            helper_function = '''

def parse_header_value(value):
    """Parse header value like cgi.parse_header() did"""
    if not value:
        return '', {}
    
    msg = Message()
    msg['content-type'] = value
    params = msg.get_params()
    
    if params:
        maintype = params[0][0]
        pdict = dict(params[1:])
        return maintype, pdict
    else:
        return value.strip(), {}

'''
            # Insertar la función helper después de los imports
            import_end = content.find('\n\nclass') if '\n\nclass' in content else content.find('\n\ndef')
            if import_end != -1:
                content = content[:import_end] + helper_function + content[import_end:]
        
        # Guardar archivo modificado
        with open(wsgi_file, 'w', encoding='utf-8') as f:
            f.write(content)
        
        print(f"✅ Archivo parcheado exitosamente")
        return True
    else:
        print(f"ℹ️  El archivo ya no usa 'import cgi'")
        return False

def patch_soap11_file(spyne_path):
    """Parchea el archivo soap11.py si es necesario"""
    soap11_file = os.path.join(spyne_path, 'protocol', 'soap', 'soap11.py')
    
    if not os.path.exists(soap11_file):
        print(f"⚠️  No se encontró: {soap11_file}")
        return False
    
    print(f"📝 Verificando: {soap11_file}")
    
    with open(soap11_file, 'r', encoding='utf-8') as f:
        content = f.read()
    
    if 'import cgi' in content and 'from email.message import Message' not in content:
        content = content.replace(
            'import cgi',
            'from email.message import Message'
        )
        
        # Si se usa cgi.parse_header, agregar la función helper
        if 'cgi.parse_header' in content:
            content = content.replace('cgi.parse_header(', 'parse_header_value(')
            
            helper_function = '''

def parse_header_value(value):
    """Parse header value like cgi.parse_header() did"""
    if not value:
        return '', {}
    
    msg = Message()
    msg['content-type'] = value
    params = msg.get_params()
    
    if params:
        maintype = params[0][0]
        pdict = dict(params[1:])
        return maintype, pdict
    else:
        return value.strip(), {}

'''
            import_end = content.find('\n\nclass') if '\n\nclass' in content else content.find('\n\ndef')
            if import_end != -1:
                content = content[:import_end] + helper_function + content[import_end:]
        
        with open(soap11_file, 'w', encoding='utf-8') as f:
            f.write(content)
        
        print(f"✅ Archivo parcheado exitosamente")
        return True
    else:
        print(f"ℹ️  El archivo no necesita parches")
        return False

def main():
    print("="*70)
    print("🔧 PARCHE PARA SPYNE - Python 3.13+ (Problema del módulo cgi)")
    print("="*70)
    
    spyne_path = find_spyne_path()
    print(f"\n📦 Spyne encontrado en: {spyne_path}")
    print(f"🐍 Python: {sys.version}\n")
    
    patched_count = 0
    
    # Parchear wsgi.py
    if patch_wsgi_file(spyne_path):
        patched_count += 1
    
    print()
    
    # Parchear soap11.py
    if patch_soap11_file(spyne_path):
        patched_count += 1
    
    print("\n" + "="*70)
    if patched_count > 0:
        print(f"✅ {patched_count} archivo(s) parcheado(s) exitosamente")
        print("\n💡 Reinicia el servidor Django para aplicar los cambios:")
        print("   python manage.py runserver")
    else:
        print("ℹ️  No se encontraron archivos que necesiten parches")
    print("="*70)

if __name__ == '__main__':
    main()
⚠️ Cuándo usar este script:
• Si usas Python 3.13 o superior
• Si obtienes el error: ModuleNotFoundError: No module named 'cgi'
• Antes de ejecutar el servidor Django por primera vez

Cómo ejecutarlo:
python fix_spyne_cgi.py

18.1.2. EJEMPLO_VISUALIZADOR_SOAP.html

📍 Ubicación: Raíz del proyecto

Ruta completa: django-sistema-u3/EJEMPLO_VISUALIZADOR_SOAP.html

💡 ¿Qué hace? Archivo HTML de ejemplo que muestra cómo se ve el visualizador de mensajes SOAP generado por cliente_soap_visual.py. Incluye un ejemplo real de Request y Response XML con diseño profesional.
EJEMPLO_VISUALIZADOR_SOAP.html
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Visualizador SOAP - Obtener libro por ID (1)</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            padding: 20px;
            color: #333;
        }

        .container {
            max-width: 1400px;
            margin: 0 auto;
            background: white;
            border-radius: 15px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            overflow: hidden;
        }

        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px 40px;
            text-align: center;
        }

        .header h1 {
            font-size: 2.5em;
            margin-bottom: 10px;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
        }

        .section-title {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 15px 25px;
            border-radius: 10px;
            font-size: 1.5em;
            margin-bottom: 20px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .xml-container {
            background: #1e1e1e;
            border-radius: 10px;
            padding: 25px;
            overflow-x: auto;
        }

        pre {
            margin: 0;
            color: #f8f8f2;
            font-family: 'Consolas', 'Monaco', monospace;
            font-size: 0.9em;
            line-height: 1.6;
        }

        .btn-copy {
            background: white;
            color: #667eea;
            border: 2px solid #667eea;
            padding: 8px 20px;
            border-radius: 20px;
            cursor: pointer;
            font-weight: 600;
        }

        .stats {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }

        .stat-card {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 10px;
            text-align: center;
        }

        .stat-card .value {
            color: #667eea;
            font-size: 1.8em;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📡 Visualizador de Mensajes SOAP</h1>
            <p>⏰ Generado: 03/02/2026 17:40:32</p>
        </div>

        <div style="padding: 40px;">
            <div class="stats">
                <div class="stat-card">
                    <div>Tamaño Request</div>
                    <div class="value">260</div>
                    <div>bytes</div>
                </div>
                <div class="stat-card">
                    <div>Tamaño Response</div>
                    <div class="value">1760</div>
                    <div>bytes</div>
                </div>
                <div class="stat-card">
                    <div>Protocolo</div>
                    <div class="value">SOAP</div>
                    <div>1.1</div>
                </div>
                <div class="stat-card">
                    <div>Formato</div>
                    <div class="value">XML</div>
                    <div>UTF-8</div>
                </div>
            </div>

            <div style="margin-bottom: 40px;">
                <div class="section-title">
                    <span>📤 Solicitud SOAP (Request)</span>
                    <button class="btn-copy" onclick="copiarTexto('request-xml')">📋 Copiar</button>
                </div>
                <div class="xml-container">
                    <pre id="request-xml">&lt;soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"&gt;
  &lt;soap-env:Body&gt;
    &lt;ns0:obtener_libro xmlns:ns0="biblioteca.soap.services"&gt;
      &lt;ns0:libro_id&gt;1&lt;/ns0:libro_id&gt;
    &lt;/ns0:obtener_libro&gt;
  &lt;/soap-env:Body&gt;
&lt;/soap-env:Envelope&gt;</pre>
                </div>
            </div>

            <div>
                <div class="section-title">
                    <span>📥 Respuesta SOAP (Response)</span>
                    <button class="btn-copy" onclick="copiarTexto('response-xml')">📋 Copiar</button>
                </div>
                <div class="xml-container">
                    <pre id="response-xml">&lt;soap11env:Envelope xmlns:soap11env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="biblioteca.soap.services" xmlns:s0="libros.soap_services"&gt;
  &lt;soap11env:Body&gt;
    &lt;tns:obtener_libroResponse&gt;
      &lt;tns:obtener_libroResult&gt;
        &lt;s0:id&gt;1&lt;/s0:id&gt;
        &lt;s0:titulo&gt;Cien años de soledad&lt;/s0:titulo&gt;
        &lt;s0:isbn&gt;9780307474728&lt;/s0:isbn&gt;
        &lt;s0:numero_paginas&gt;471&lt;/s0:numero_paginas&gt;
        &lt;s0:idioma&gt;Español&lt;/s0:idioma&gt;
        &lt;s0:descripcion&gt;Obra maestra del realismo mágico.&lt;/s0:descripcion&gt;
        &lt;s0:estado&gt;prestado&lt;/s0:estado&gt;
        &lt;s0:stock_total&gt;5&lt;/s0:stock_total&gt;
        &lt;s0:stock_disponible&gt;0&lt;/s0:stock_disponible&gt;
        &lt;s0:autor&gt;
          &lt;s0:id&gt;1&lt;/s0:id&gt;
          &lt;s0:nombre&gt;Gabriel&lt;/s0:nombre&gt;
          &lt;s0:apellido&gt;García Márquez&lt;/s0:apellido&gt;
          &lt;s0:nacionalidad&gt;Colombiano&lt;/s0:nacionalidad&gt;
          &lt;s0:biografia&gt;Premio Nobel de Literatura 1982.&lt;/s0:biografia&gt;
        &lt;/s0:autor&gt;
        &lt;s0:editorial&gt;
          &lt;s0:id&gt;1&lt;/s0:id&gt;
          &lt;s0:nombre&gt;Editorial Sudamericana&lt;/s0:nombre&gt;
          &lt;s0:pais&gt;Argentina&lt;/s0:pais&gt;
        &lt;/s0:editorial&gt;
        &lt;s0:categoria&gt;
          &lt;s0:id&gt;1&lt;/s0:id&gt;
          &lt;s0:nombre&gt;Ficción&lt;/s0:nombre&gt;
        &lt;/s0:categoria&gt;
      &lt;/tns:obtener_libroResult&gt;
    &lt;/tns:obtener_libroResponse&gt;
  &lt;/soap11env:Body&gt;
&lt;/soap11env:Envelope&gt;</pre>
                </div>
            </div>
        </div>

        <div style="background: #2d3436; color: white; padding: 20px; text-align: center;">
            <p>🎓 Sistema de Biblioteca - Cliente SOAP Interactivo</p>
            <p>📚 Guía de Servicios Web - 2026</p>
        </div>
    </div>

    <script>
        function copiarTexto(elementId) {
            const elemento = document.getElementById(elementId);
            const texto = elemento.textContent;
            
            navigator.clipboard.writeText(texto).then(() => {
                alert('✅ XML copiado al portapapeles');
            }).catch(err => {
                console.error('Error al copiar:', err);
            });
        }
    </script>
</body>
</html>
Vista previa disponible: Puedes abrir este archivo HTML directamente en tu navegador para ver cómo se visualizan los mensajes SOAP. Es un ejemplo de lo que genera automáticamente el cliente_soap_visual.py después de cada operación.
💡 Funcionalidades del visualizador:
• Muestra el XML Request y Response lado a lado
• Incluye estadísticas de tamaño de los mensajes
• Botones para copiar el XML al portapapeles
• Diseño responsivo y profesional
• Syntax highlighting para facilitar la lectura

18.2. ejemplos_admin.html

📍 Ubicación: Carpeta templates del proyecto

Ruta completa: django-sistema-u3/biblioteca_project/templates/ejemplos_admin.html

💡 ¿Qué hace? Página HTML con ejemplos y guía de uso del panel administrativo Django.
⚠️ Nota: Este archivo es muy largo (524 líneas). Puedes descargarlo desde: ejemplos_admin.html o copiar el código del archivo incluido en el proyecto.

Contenido principal:
• Guía de acceso al panel admin
• Gestión de libros, autores, préstamos
• Gestión de usuarios
• Categorías y editoriales
• Casos de uso prácticos
• Permisos y seguridad

18.3. ejemplos_rest.html

📍 Ubicación: Carpeta templates del proyecto

Ruta completa: django-sistema-u3/biblioteca_project/templates/ejemplos_rest.html

💡 ¿Qué hace? Página HTML interactiva con ejemplos de uso del API REST con pruebas en vivo.
⚠️ Nota: Este archivo es muy largo (528 líneas con JavaScript). Puedes descargarlo desde: ejemplos_rest.html o copiar el código del archivo incluido en el proyecto.

Contenido principal:
• Ejemplos de endpoints REST
• Operaciones CRUD de libros
• Gestión de préstamos
• Filtros y búsquedas
• Paginación
• Casos de uso completos
• Botones interactivos para probar API

18.4. ejemplos_soap.html

📍 Ubicación: Carpeta templates del proyecto

Ruta completa: django-sistema-u3/biblioteca_project/templates/ejemplos_soap.html

💡 ¿Qué hace? Página HTML con ejemplos de mensajes SOAP, operaciones disponibles y código Python.
⚠️ Nota: Este archivo tiene 401 líneas. Puedes descargarlo desde: ejemplos_soap.html o copiar el código del archivo incluido en el proyecto.

Contenido principal:
• ¿Qué es SOAP?
• Acceso al WSDL
• Operaciones disponibles (obtener_libro, listar_libros, crear_prestamo, etc.)
• Ejemplos de Request/Response XML
• Código Python con zeep
• Casos de uso SOAP
• Herramientas recomendadas (SoapUI, Postman)

¡Archivos listos! Todos los archivos auxiliares están disponibles para copiar y pegar en las ubicaciones indicadas. Estos archivos complementan el sistema con ejemplos interactivos y herramientas de testing.

19. Funcionalidades Extras y Mejoras

19.1. Código fuente completo

ℹ️ Accede directamente a los archivos HTML con todo el código fuente completo:

📦 Contenido de cada archivo:

  • 📄 TEMPLATES_COMPLETOS.html: Todos los templates (base.html, home.html, ejemplos, etc.)
  • 🔧 VIEWS_URLS_COMPLETOS.html: views.py, urls.py, serializers.py, admin.py completos
  • 🎨 CSS_JS_COMPLETOS.html: styles.css y main.js con todo el código frontend

19.2. Detener el servidor

Para detener el servidor Django

Presiona Ctrl + C en la terminal donde está corriendo el servidor.

19.3. Desactivar entorno virtual

Cuando termines de trabajar
deactivate

Esto desactiva el entorno virtual. Recuerda activarlo de nuevo la próxima vez con venv311\Scripts\activate

19.4. Comandos útiles de Django

Comando Descripción
python manage.py makemigrations Crear archivos de migración
python manage.py migrate Aplicar migraciones a la BD
python manage.py createsuperuser Crear usuario administrador
python manage.py runserver Iniciar servidor de desarrollo
python manage.py shell Abrir consola interactiva de Django
python manage.py collectstatic Recolectar archivos estáticos
python populate_db.py Poblar base de datos con datos de prueba
python cliente_soap_visual.py Ejecutar cliente SOAP interactivo

19.5. Resumen de tecnologías usadas

Stack tecnológico del proyecto
Tecnología Versión Propósito
Python 3.11 Lenguaje de programación
Django 5.2.10 Framework web
MySQL 8.0 Base de datos relacional
Django REST Framework 3.14.0 API RESTful
Spyne 2.14.0 Servicio web SOAP
Zeep 4.2.1 Cliente SOAP
PyMySQL 1.1.1 Conector MySQL
Docker Latest Contenedores (opcional)
Nginx Alpine Servidor web (con Docker)
Redis 7 Caché (con Docker)

19.6. Estructura de archivos del proyecto final

📁 django-sistema-u3/
├── 📁 venv311/ (entorno virtual)
├── 📁 biblioteca_project/ (configuración)
│ ├── __init__.py
│ ├── settings.py (configuración principal)
│ ├── urls.py (rutas principales)
│ ├── views.py (vistas de templates)
│ ├── wsgi.py
│ ├── asgi.py
│ └── 📁 templates/ (plantillas HTML)
│ ├── home.html
│ ├── ejemplos_rest.html
│ ├── ejemplos_soap.html
│ └── ejemplos_admin.html
├── 📁 libros/ (app principal)
│ ├── __init__.py
│ ├── admin.py (configuración del admin)
│ ├── models.py (modelos de datos)
│ ├── views.py (vistas de API REST)
│ ├── urls.py (rutas de API)
│ ├── serializers.py (serializadores REST)
│ ├── soap_services.py (servicio SOAP)
│ └── 📁 migrations/ (migraciones de BD)
├── 📁 static/ (archivos estáticos)
│ ├── 📁 css/
│ │ └── styles.css
│ └── 📁 js/
│ └── main.js
├── 📁 staticfiles/ (archivos estáticos recolectados)
├── manage.py (script de Django)
├── populate_db.py (script de datos)
├── cliente_soap_visual.py (cliente SOAP)
├── requirements.txt (dependencias)
├── Dockerfile (imagen Docker)
├── docker-compose.yml (orquestación)
├── docker-compose.scale.yml (escalado)
└── nginx.conf (config Nginx)

19.7. Endpoints del sistema

URL Tipo Descripción
/ HTML Página principal del sistema
/admin/ HTML Panel administrativo de Django
/api/ REST Raíz de la API REST (navegable)
/api/libros/ REST CRUD de libros
/api/autores/ REST CRUD de autores
/api/categorias/ REST CRUD de categorías
/api/editoriales/ REST CRUD de editoriales
/api/prestamos/ REST CRUD de préstamos
/soap/ SOAP Servicio web SOAP
/soap/?wsdl WSDL Definición del servicio SOAP
/ejemplos/rest/ HTML Ejemplos de uso de REST API
/ejemplos/soap/ HTML Ejemplos de uso de SOAP
/ejemplos/admin/ HTML Ejemplos del panel admin

19.8. Verificar instalación completa

Checklist final
  • Python 3.11 instalado y funcionando
  • MySQL instalado y base de datos creada
  • Entorno virtual creado y activado
  • Todas las dependencias instaladas
  • Proyecto Django creado
  • Modelos definidos y migrados
  • Superusuario creado
  • Datos de prueba cargados
  • Servidor corriendo sin errores
  • Página principal accesible
  • Panel admin accesible
  • API REST funcionando
  • Servicio SOAP activo
¡Felicidades! Has completado la instalación del Sistema de Gestión de Biblioteca.
El proyecto está completamente funcional y listo para usar.

🎬 Checklist de Verificación Final (Para Grabación)

⚠️ IMPORTANTE: Al finalizar la grabación, se debe comprobar el funcionamiento COMPLETO del sistema siguiendo este orden EXACTO. Cada paso debe ejecutarse exitosamente antes de pasar al siguiente. Esta verificación demuestra que TODO el sistema funciona correctamente.

1 Panel de Administración Django

📁 Archivo: ejemplos_admin.html

Qué comprobar:

  • ✅ Acceder a http://127.0.0.1:8000/admin/
  • ✅ Iniciar sesión con admin / admin123
  • ✅ Ver todas las secciones: Libros, Autores, Usuarios, Préstamos, Categorías, Editoriales
  • ✅ Abrir un libro y verificar que se visualizan todos los campos
  • ✅ Verificar que hay datos precargados (al menos 7 libros)
✔️ Resultado esperado: Panel admin funcional con todos los modelos visibles y datos precargados

2 API REST - Navegador

📁 Archivo: ejemplos_rest.html

Qué comprobar (mínimo 5 pruebas):

  • GET Listar todos los libros: /api/libros/
  • GET Buscar libros: /api/libros/?search=garcía
  • GET Libro específico: /api/libros/1/
  • GET Listar autores: /api/autores/
  • GET Listar préstamos: /api/prestamos/
✔️ Resultado esperado: Todas las respuestas en formato JSON visualizadas correctamente en el navegador

3 Servicio SOAP - Navegador

📁 Archivo: ejemplos_soap.html

Qué comprobar:

  • ✅ Acceder al WSDL: http://127.0.0.1:8000/soap/?wsdl
  • ✅ Verificar que el XML WSDL se genera correctamente
  • ✅ Comprobar que se listan las 3 operaciones:
    • obtener_libro
    • listar_libros
    • crear_prestamo
  • ✅ Ver ejemplos de Request/Response XML en la guía
✔️ Resultado esperado: WSDL accesible y operaciones SOAP documentadas

4 Testing con Postman 2026

📁 Archivo: GUIA_POSTMAN_2026.html

Qué comprobar (mínimo 3 peticiones):

A) REST API en Postman:

  • GET http://127.0.0.1:8000/api/libros/
  • GET http://127.0.0.1:8000/api/libros/?search=amor
  • ✅ Verificar respuestas JSON con status 200

B) SOAP en Postman:

  • ✅ Importar WSDL: http://127.0.0.1:8000/soap/?wsdl
  • ✅ Ejecutar operación obtener_libro con ID 1
  • ✅ Verificar respuesta XML con datos del libro
✔️ Resultado esperado: Postman instalado, peticiones REST y SOAP exitosas

5 Docker Desktop - Contenedores

🐳 Docker Desktop

Qué comprobar:

docker-compose up --build
  • ✅ Docker Desktop está ejecutándose (ícono verde)
  • ✅ Ejecutar docker-compose up --build
  • ✅ Verificar que se levantan 3 contenedores:
    • mysql_db (base de datos)
    • django_app (aplicación web)
    • nginx (servidor web)
  • ✅ Acceder a http://localhost/admin/
  • ✅ Verificar que el sistema funciona en Docker
  • ✅ Ver logs sin errores: docker-compose logs -f
✔️ Resultado esperado: Contenedores ejecutándose, sistema accesible en puerto 80

6 Cliente SOAP Visual - 3 Consultas XML

📁 Archivo: cliente_soap_visual.py
python cliente_soap_visual.py
REALIZAR ESTAS 3 CONSULTAS EN EL MENÚ INTERACTIVO:

📋 CONSULTA 1: Obtener Libro ID 1

Opción del menú: 1 - Obtener libro por ID

ID a ingresar: 1

Verificar que se genera:

  • ✅ Archivo HTML: soap_obtener_libro_[timestamp].html
  • ✅ Se abre automáticamente en el navegador
  • ✅ Muestra XML Request y XML Response completos
  • ✅ Datos del libro: "Cien años de soledad"
  • ✅ Autor: "Gabriel García Márquez"
  • ✅ ISBN: "9780307474728"

📋 CONSULTA 2: Listar Todos los Libros

Opción del menú: 2 - Listar todos los libros

Verificar que se genera:

  • ✅ Archivo HTML: soap_listar_libros_[timestamp].html
  • ✅ Se abre automáticamente en el navegador
  • ✅ XML Response contiene múltiples elementos <libro>
  • ✅ Al menos 7 libros listados con sus títulos
  • ✅ Cada libro muestra: ID, título, autor

📋 CONSULTA 3: Crear Préstamo

Opción del menú: 3 - Crear nuevo préstamo

Datos a ingresar:

  • ID del libro: 4
  • ID del usuario: 2
  • Días de préstamo: 14

Verificar que se genera:

  • ✅ Archivo HTML: soap_crear_prestamo_[timestamp].html
  • ✅ Se abre automáticamente en el navegador
  • ✅ XML Request muestra los 3 parámetros enviados
  • ✅ XML Response contiene:
    • prestamo_id (número del nuevo préstamo)
    • mensaje = "Préstamo creado exitosamente"
    • fecha_devolucion (14 días después)
  • ✅ Mensaje de éxito en consola

📁 Archivos HTML generados:

  • Se guardan en la carpeta raíz del proyecto
  • Nombre con timestamp para no sobrescribir
  • Se abren automáticamente en el navegador predeterminado
  • Contienen XML Request y Response formateados y coloreados
✔️ Resultado esperado: 3 archivos HTML generados mostrando XML completo de cada operación SOAP

🎯 RESUMEN DE VERIFICACIÓN

6
Pasos Completos
4
Archivos HTML
3
Consultas SOAP
2
APIs (REST + SOAP)

✅ CHECKLIST FINAL

  • ☑️ Panel de administración Django funcional
  • ☑️ API REST respondiendo correctamente
  • ☑️ Servicio SOAP con WSDL accesible
  • ☑️ Postman probando REST y SOAP exitosamente
  • ☑️ Docker Desktop con contenedores ejecutándose
  • ☑️ Cliente SOAP Visual generando 3 HTMLs con XML

🎉 SISTEMA COMPLETAMENTE VERIFICADO 🎉

Todos los componentes funcionando correctamente

20. 🚀 Despliegue en Producción (GitHub + PythonAnywhere)

⚠️ IMPORTANTE: Esta sección es para llevar tu proyecto a producción. El sistema debe estar completamente funcional en local antes de proceder con el despliegue.
ℹ️ Objetivo: Subir el proyecto a GitHub y desplegarlo en PythonAnywhere para que esté accesible desde internet. Solo probaremos UN EJEMPLO de cada sección anterior (REST, SOAP, Admin) ya que de manera local ya se probaron todas las funcionalidades.

20.1. Preparar el Proyecto para Producción

1 Crear archivo .gitignore

Ubicación: Raíz del proyecto django-sistema-u3-guia/.gitignore

.gitignore
# Python
*.pyc
__pycache__/
*.py[cod]
*$py.class

# Virtual Environment
venv/
venv311/
env/
ENV/

# Django
*.log
db.sqlite3
db.sqlite3-journal
/staticfiles/
/media/

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Environment variables
.env

# Docker
*.exe
Docker Desktop Installer.exe

# Docs temporales
*.docx
~$*
2 Crear archivo README.md para GitHub
README.md
# 📚 Sistema de Gestión de Biblioteca

Sistema web completo desarrollado con Django, MySQL, REST API y SOAP.

## 🚀 Características

- ✅ Panel de Administración Django
- ✅ API REST con Django REST Framework
- ✅ Servicio Web SOAP con Spyne
- ✅ Gestión completa de libros, autores, préstamos
- ✅ Cliente SOAP interactivo con visualizador XML
- ✅ Docker y Docker Compose
- ✅ Base de datos MySQL

## 📋 Requisitos

- Python 3.11
- MySQL 8.0
- Docker Desktop (opcional)

## 🔧 Instalación Local

Ver archivo `GUIA_INSTALACION_COMPLETA.html` para instrucciones detalladas.

## 🌐 Demo en Producción

El sistema está desplegado en PythonAnywhere:
- URL: [TU_URL_PYTHONANYWHERE]
- Usuario Admin: admin / admin123

## 📖 Documentación

- `GUIA_INSTALACION_COMPLETA.html` - Guía completa paso a paso
- `GUIA_POSTMAN_2026.html` - Testing con Postman
- `GUIA_SOAP_WSDL.md` - Documentación SOAP

## 👨‍💻 Autor

[TU NOMBRE] - Servicios Web 2026

20.2. Subir el Proyecto a GitHub

1 Crear repositorio en GitHub

1. Ve a https://github.com e inicia sesión (o crea cuenta)
2. Haz clic en el botón verde "New" o "+""New repository"
3. Nombre del repositorio: django-sistema-biblioteca
4. Descripción: Sistema de gestión de biblioteca con Django, REST y SOAP
5. Selecciona "Public" (o Private si prefieres)
6. NO marques "Add a README file" (ya lo crearemos)
7. Haz clic en "Create repository"

2 Inicializar Git en tu proyecto local

Abre la terminal en la carpeta raíz de tu proyecto:

# Inicializar repositorio Git git init
# Agregar todos los archivos (excepto los del .gitignore) git add .
# Hacer el primer commit git commit -m "Initial commit - Sistema Biblioteca completo"
# Conectar con el repositorio remoto de GitHub git remote add origin https://github.com/TU_USUARIO/django-sistema-biblioteca.git
# Subir los archivos a GitHub git branch -M main git push -u origin main
💡 Nota: Reemplaza TU_USUARIO con tu nombre de usuario de GitHub. Te pedirá autenticación (usa tu usuario y token personal de GitHub).
¡Proyecto en GitHub! Tu código ahora está en la nube y versionado. Puedes verlo en: https://github.com/TU_USUARIO/django-sistema-biblioteca

20.3. Desplegar en PythonAnywhere

ℹ️ ¿Qué es PythonAnywhere? Es una plataforma de hosting gratuita para aplicaciones Python/Django. Perfecta para proyectos educativos y pruebas en producción.
1 Crear cuenta en PythonAnywhere

1. Ve a https://www.pythonanywhere.com
2. Haz clic en "Pricing & signup"
3. Selecciona el plan "Beginner (Free)"
4. Crea tu cuenta (username, email, contraseña)
5. Verifica tu email
6. Inicia sesión en PythonAnywhere

2 Clonar el repositorio desde GitHub

1. En el dashboard de PythonAnywhere, ve a la pestaña "Consoles"
2. Crea una nueva consola: "$ Bash"
3. Ejecuta estos comandos:

# Clonar tu repositorio git clone https://github.com/TU_USUARIO/django-sistema-biblioteca.git
# Entrar a la carpeta del proyecto cd django-sistema-biblioteca
# Crear entorno virtual python3.11 -m venv venv
# Activar entorno virtual source venv/bin/activate
# Instalar dependencias pip install -r requirements.txt
3 Configurar la base de datos MySQL en PythonAnywhere

1. En el dashboard, ve a la pestaña "Databases"
2. En "MySQL", haz clic en "Initialize MySQL"
3. Ingresa una contraseña y confírmala
4. Crea una nueva base de datos: biblioteca_dbutres
5. Anota los datos de conexión que te muestra

4 Actualizar settings.py para producción

En la consola Bash de PythonAnywhere, edita el archivo settings.py:

nano biblioteca_project/settings.py

Cambios necesarios:

# 1. DEBUG en False para producción
DEBUG = False

# 2. ALLOWED_HOSTS con tu dominio de PythonAnywhere
ALLOWED_HOSTS = ['TU_USUARIO.pythonanywhere.com', 'localhost', '127.0.0.1']

# 3. DATABASES con los datos de PythonAnywhere
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'TU_USUARIO$biblioteca_dbutres',
        'USER': 'TU_USUARIO',
        'PASSWORD': 'TU_PASSWORD_MYSQL',
        'HOST': 'TU_USUARIO.mysql.pythonanywhere-services.com',
        'PORT': '3306',
    }
}

Guarda con Ctrl + O, Enter, y sal con Ctrl + X

5 Ejecutar migraciones y crear superusuario
# Aplicar migraciones python manage.py migrate
# Crear superusuario python manage.py createsuperuser
# Recolectar archivos estáticos python manage.py collectstatic --noinput
# Poblar base de datos python populate_db.py
6 Configurar la aplicación web

1. Ve a la pestaña "Web" en el dashboard
2. Haz clic en "Add a new web app"
3. Selecciona "Manual configuration"
4. Elige "Python 3.11"
5. Configura estos campos:

Campo Valor
Source code /home/TU_USUARIO/django-sistema-biblioteca
Working directory /home/TU_USUARIO/django-sistema-biblioteca
Virtualenv /home/TU_USUARIO/django-sistema-biblioteca/venv
7 Configurar WSGI file

1. En la pestaña "Web", haz clic en el link del archivo WSGI
2. Borra todo el contenido
3. Pega este código:

import os
import sys

# Agregar la ruta del proyecto
path = '/home/TU_USUARIO/django-sistema-biblioteca'
if path not in sys.path:
    sys.path.append(path)

# Configurar Django settings
os.environ['DJANGO_SETTINGS_MODULE'] = 'biblioteca_project.settings'

# Importar la aplicación WSGI de Django
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

4. Reemplaza TU_USUARIO con tu username de PythonAnywhere
5. Haz clic en "Save"

8 Configurar archivos estáticos

En la sección "Static files" de la pestaña "Web":

URL Directory
/static/ /home/TU_USUARIO/django-sistema-biblioteca/staticfiles
/media/ /home/TU_USUARIO/django-sistema-biblioteca/media
9 Recargar la aplicación

1. Vuelve arriba en la pestaña "Web"
2. Haz clic en el botón verde "Reload TU_USUARIO.pythonanywhere.com"
3. Espera 10-15 segundos

¡Sistema desplegado en producción!
Accede a: https://TU_USUARIO.pythonanywhere.com

20.4. Pruebas en Producción (UN EJEMPLO de cada sección)

⚠️ Importante: Solo probaremos UN EJEMPLO de cada funcionalidad ya que en local ya se probaron exhaustivamente todas las características.
✅ Prueba 1: Panel Admin
  • Acceder a https://TU_USUARIO.pythonanywhere.com/admin/
  • Login con tu superusuario
  • Verificar que se ven los libros listados
✅ Prueba 2: API REST
  • Acceder a https://TU_USUARIO.pythonanywhere.com/api/libros/
  • Verificar que devuelve JSON con los libros
✅ Prueba 3: Servicio SOAP
  • Acceder a https://TU_USUARIO.pythonanywhere.com/soap/?wsdl
  • Verificar que se genera el XML WSDL correctamente
💡 Nota: Con estas 3 pruebas simples se verifica que el sistema funciona en producción. No es necesario repetir todas las pruebas exhaustivas hechas en local (cliente SOAP, Postman, etc.) ya que la funcionalidad es la misma, solo cambia el entorno de ejecución.

20.5. Mantenimiento y Actualización

Para actualizar el código en producción

Cuando hagas cambios locales y los subas a GitHub:

# En tu computadora local git add . git commit -m "Descripción de cambios" git push origin main
# En PythonAnywhere (consola Bash) cd django-sistema-biblioteca git pull origin main source venv/bin/activate pip install -r requirements.txt python manage.py migrate python manage.py collectstatic --noinput

Luego recarga la aplicación desde la pestaña "Web" → botón "Reload"

¡Sistema en producción completo!
Tu proyecto ahora está:
• 📦 Versionado en GitHub
• 🌐 Desplegado en PythonAnywhere
• ✅ Accesible desde cualquier lugar del mundo
• 🚀 Listo para compartir con tu profesor y compañeros
💡 Ventajas de tener el sistema en producción:
• Tu profesor puede ver el proyecto funcionando sin instalar nada
• Puedes compartir el enlace en tu CV o portafolio
• Demuestras conocimiento de despliegue profesional
• El proyecto queda documentado en GitHub como parte de tu experiencia