1. Requisitos Previos y Verificación del Sistema
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
En Windows:
• Presiona las teclas Windows + R
• Escribe cmd y presiona Enter
• Se abrirá una ventana negra (la terminal)
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
2.1. Descargar Python
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"
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"
1. Cierra cualquier terminal que tengas abierta
2. Abre una nueva terminal (Windows + R, luego "cmd")
3. Ejecuta:
Deberías ver algo como:
Python 3.11.9
pip 24.0 from ...
3. Instalación y Configuración de MySQL
3.1. Descargar 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
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"
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
Recuerda tu contraseña: _________________________
3.3. Crear la Base de Datos
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
Ejecuta los siguientes comandos uno por uno:
# Verificar que se creó SHOW DATABASES;
# Salir de MySQL exit;
Deberías ver "biblioteca_dbutres" en la lista de bases de datos.
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)
4.1. ¿Por qué es obligatorio Docker para este proyecto?
- 🎯 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
4.2. Requisitos previos para Docker
| 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 |
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
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)
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
Paso a paso detallado:
- Haz doble clic en el archivo "Docker Desktop Installer.exe"
- Si aparece la ventana de "Control de Cuentas de Usuario", haz clic en "Sí"
- Aparecerá la ventana de configuración de Docker Desktop
⚠️ 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
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
Cuando termine la instalación, aparecerá un mensaje similar a este:
✅ "Installation succeeded"
⚠️ "Docker Desktop requires a reboot to complete the installation"
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
Cuando Windows vuelva a iniciar:
- Busca el ícono de Docker en el escritorio (ballena azul con contenedores) 🐋
- Haz doble clic para abrir Docker Desktop
Si no está en el escritorio, presionaWindowsy busca "Docker Desktop" - Espera unos 10-20 segundos mientras Docker inicia por primera vez
- Aparecerá la ventana de bienvenida de Docker Desktop
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"
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
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
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 ✅
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. Presiona las teclas Windows + R
2. Escribe cmd
3. Presiona Enter
4. Se abrirá la ventana negra de comandos (CMD)
Copia y pega este comando en la ventana negra:
Salida esperada (algo similar a):
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.
Docker Compose es necesario para orquestar múltiples contenedores:
Salida esperada:
Si ves la versión, significa que Docker Compose está instalado correctamente (viene incluido con Docker Desktop).
Este comando descarga y ejecuta un contenedor de prueba simple para verificar que todo funciona:
¿Qué pasará? (paso a paso):
- Docker buscará la imagen "hello-world" en tu computadora (no la encontrará)
- La descargará automáticamente de Docker Hub (solo ~2 KB, toma 2-3 segundos)
- Verás:
Unable to find image 'hello-world:latest' locally - Luego:
latest: Pulling from library/hello-world - Ejecutará el contenedor automáticamente
- Mostrará un mensaje de éxito
- El contenedor se detendrá automáticamente (ya hizo su trabajo)
Salida esperada completa:
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...
Para ver todos los contenedores que se han ejecutado (incluso los que ya terminaron):
Deberías ver una tabla con el contenedor "hello-world" en estado Exited (0):
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
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
Si tu PC tiene 8 GB o más de RAM, puedes asignar más recursos a Docker para mejor rendimiento:
- Abre Docker Desktop (si lo cerraste)
- Haz clic en el ícono de engranaje ⚙️ (Settings) en la parte superior derecha
- Ve a la sección "Resources" → "Advanced" (en el menú izquierdo)
- 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) - Haz clic en "Apply & restart" en la parte inferior derecha
- Espera unos segundos mientras Docker se reinicia
Asegúrate de que Docker esté integrado correctamente con WSL 2:
- En Docker Desktop, haz clic en Settings ⚙️
- Ve a "Resources" → "WSL Integration"
- Verifica que esta opción esté activada:
☑️ "Enable integration with my default WSL distro" - Si aparece "Ubuntu" en la lista de distribuciones, actívala también:
☑️ Ubuntu - Haz clic en "Apply & restart"
4.8. ¿Qué hace nuestro sistema 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.
┌─────────────────────────────────────────────────────────┐
│ 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)
| 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
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)
Solución:
# Luego reinicia Docker Desktop
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
• 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
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
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
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. Abre una terminal (CMD)
2. Navega a donde quieres crear el proyecto (ejemplo: Escritorio):
# Crear carpeta del proyecto mkdir django-sistema-u3
# Entrar a la carpeta cd django-sistema-u3
5.2. Crear entorno virtual
# 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.
(venv311), ejecuta de nuevo
venv311\Scripts\activate
5.3. Instalar dependencias
Asegúrate de que el entorno virtual esté activado (debe aparecer (venv311)),
luego ejecuta:
# 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).
5.4. Crear proyecto Django
El punto (.) al final es importante: indica que se cree en la carpeta actual.
Abre el archivo biblioteca_project/__init__.py con tu editor de texto
(Notepad, VS Code, etc.) y agrega estas líneas:
import pymysql pymysql.install_as_MySQLdb()
Guarda el archivo. Esto permite que Django use pymysql como conector de MySQL.
5. Estructura Completa del Proyecto
5.1. Estructura actual (después de los pasos anteriores)
├── 📁 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
Ejecuta estos comandos en la terminal (desde la carpeta django-sistema-u3):
# Crear carpetas para archivos estáticos mkdir static mkdir static\css mkdir static\js
5.3. Estructura final completa
├── 📁 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. 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:
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',
]
En el mismo archivo settings.py, busca la sección DATABASES
(alrededor de la línea 76) y reemplázala completamente con esto:
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',
},
}
}
'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',
Busca USE_TZ y TIME_ZONE en settings.py y modifícalos:
USE_TZ = False TIME_ZONE = 'America/Hermosillo' # O tu zona horaria
Busca la sección TEMPLATES en settings.py y modifica DIRS:
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',
],
},
},
]
Al final del archivo settings.py, busca STATIC_URL y agrega debajo:
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
Al final del archivo settings.py, agrega esta configuración:
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.
7. Creación de Modelos
7.1. Crear modelos en libros/models.py
Abre el archivo libros/models.py y reemplaza todo su contenido con:
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.
• 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
8.1. Crear y aplicar migraciones
En la terminal (asegúrate de estar en la carpeta django-sistema-u3 y que el entorno virtual esté activado), ejecuta:
Deberías ver algo como:
Esto creará todas las tablas en MySQL. Verás muchas líneas que dicen "OK" en verde.
Puedes verificar que las tablas se crearon correctamente:
Deberías ver tablas como: libros_autor, libros_libro, libros_prestamo, etc.
9. Configuración de API REST
9.1. Crear serializers.py
Crea un nuevo archivo libros/serializers.py con este contenido:
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
Abre libros/views.py y reemplaza su contenido con:
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
Crea el archivo libros/urls.py con:
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
✅ 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.
Crea el archivo libros/soap_services.py con el siguiente código completo:
"""
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
# ===== 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
# ===== 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))
• 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
11. Creación de Templates HTML
11.1. Crear views.py principal
Crea el archivo 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
Crea el archivo biblioteca_project/templates/home.html.
Puedes obtener el código completo desde el archivo de templates.
home.html y todos los
templates de ejemplos (ejemplos_rest.html, ejemplos_soap.html, ejemplos_admin.html).
11.3. Crear templates de ejemplos
Ya tienes los templates de ejemplos creados en tu proyecto. Puedes acceder a ellos directamente desde estos botones:
Ver ejemplos de endpoints 🔌 Ejemplos SOAP
Ver ejemplos del servicio SOAP 👨💼 Ejemplos Admin
Ver ejemplos del panel admin
biblioteca_project/templates/
12. Archivos Estáticos (CSS y JavaScript)
12.1. Crear styles.css y main.js
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
12.2. Crear las carpetas y archivos
Asegúrate de crear los archivos en la ubicación correcta:
static/css/styles.css- Estilos del sistemastatic/js/main.js- JavaScript interactivo
12.3. Recolectar archivos estáticos
Una vez que hayas copiado los archivos CSS y JS, ejecuta:
Este comando recopila todos los archivos estáticos en la carpeta staticfiles/
13. Configuración del Panel Admin
13.1. Crear admin.py
Abre libros/admin.py y reemplaza su contenido con:
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
Te pedirá:
• Username: admin
• Email: admin@biblioteca.com (o déjalo en blanco)
• Password: admin123 (escríbelo dos veces)
14. Configuración de URLs
Abre biblioteca_project/urls.py y reemplaza su contenido con:
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
✅ 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)
Crea el archivo populate_db.py en la raíz del proyecto:
"""
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
Esto creará libros, autores, categorías y usuarios de prueba.
16. Ejecución y Pruebas del Sistema
16.1. Iniciar el servidor
Deberías ver algo como:
16.2. Probar las páginas
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
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
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
16.5. Pruebas del Servicio SOAP (cliente_soap_visual.py)
16.5.1. Instalar dependencia zeep
Con el entorno virtual activado, instala zeep:
# Instalar zeep pip install zeep
16.5.2. Verificar que el servidor esté corriendo
En una terminal, asegúrate de que el servidor Django esté corriendo:
16.5.3. Ejecutar el cliente SOAP interactivo
1. Abre una NUEVA terminal (CMD)
2. Navega a la carpeta del proyecto:
# Activar el entorno virtual venv311\Scripts\activate
# Ejecutar el cliente SOAP python cliente_soap_visual.py
16.5.4. Usar el menú interactivo
El cliente mostrará un menú como este:
================================================================================ 🏛️ 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
1. Selecciona la opción 1
2. Presiona Enter
3. El sistema mostrará todos los libros con ID, título, autor y stock
📚 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
1. Selecciona la opción 3
2. Ingresa parte del título, por ejemplo: soledad
3. Presiona Enter
🔍 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
--------------------------------------------------------------------------------
1. Selecciona la opción 2
2. Ingresa el ID del libro, por ejemplo: 1
3. Presiona Enter
🔍 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
1. Selecciona la opción 4
2. Ingresa el apellido del autor: García Márquez
3. Presiona Enter
👤 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
--------------------------------------------------------------------------------
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
➕ 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
1. Selecciona la opción 10
2. Presiona Enter
📋 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
• 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
También puedes ver la definición WSDL del servicio en:
Esto te mostrará el XML con la especificación completa del servicio SOAP.
16.6. Pruebas de APIs REST y SOAP con Postman
16.6.1. Instalación de 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
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)
juan_garcia_lopez o garcia.lopez.api
Cuando Postman se abra por primera vez, verás una pantalla de bienvenida:
- Haz clic en "Create Account" o "Sign Up"
- Completa el formulario:
• Email: Tu correo electrónico
• Username:TUS_APELLIDOS_API(ejemplo:garcia_lopez_api)
• Password: Una contraseña segura
- Marca la casilla de aceptar términos y condiciones
- Haz clic en "Create free account"
- Verifica tu email (revisa tu bandeja de entrada y spam)
- Haz clic en el enlace de verificación del correo
• garcia_lopez_api
• ramirez.martinez.dev
• hernandez_gonzalez_2024
• lopez-sanchez-sw
Después de iniciar sesión:
- Te preguntará sobre tu rol → Selecciona "Student" o "Developer"
- Te preguntará qué quieres hacer → Selecciona "Test APIs"
- Puedes omitir el tutorial (Skip) o hacerlo si lo deseas
- Ya estás en la pantalla principal de Postman ✅
16.6.3. Crear Workspace para el proyecto
1. En la esquina superior izquierda, haz clic en "Workspaces"
2. Haz clic en "Create Workspace"
3. Completa:
Sistema Biblioteca U3• Summary:
APIs REST y SOAP para Sistema de Biblioteca• Visibility:
Personal (solo tú puedes verlo)
4. Haz clic en "Create Workspace"
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)
Asegúrate de que el servidor Django esté corriendo:
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:
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
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)
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"
16.6.6. Probar API REST - Buscar Libros (GET con Query Params)
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:
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. 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"
settings.py a
'rest_framework.permissions.AllowAny' para pruebas.
201 Created y el JSON del libro creado con su ID asignado.
16.6.8. Probar API REST - Actualizar un Libro (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)
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)
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)
16.6.11. Probar Otros Endpoints REST
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
1. Crea una nueva collection: Biblioteca API - SOAP
2. Agrega descripción: Pruebas de servicio SOAP
1. Nueva request: SOAP - Listar libros
2. Método: POST
3. URL: http://127.0.0.1:8000/soap/
4. Pestaña "Headers" → Agregar:
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
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
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
Para guardar todas tus pruebas:
- Haz clic derecho en la collection "Biblioteca API - REST"
- Selecciona "Export"
- Elige "Collection v2.1"
- Haz clic en "Export"
- Guarda el archivo JSON en tu proyecto
- Repite para la collection "Biblioteca API - SOAP"
16.6.14. Resumen de Endpoints Probados
• 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 ✅
16.7. Despliegue con Docker (Producción Avanzada)
16.6.1. ¿Por qué usar Docker para este proyecto?
- 🚀 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
django-sistema-u3/) para poder usar Docker.
En la raíz del proyecto (django-sistema-u3/), crea un archivo llamado
Dockerfile (sin extensión) y agrega el siguiente contenido:
# 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"]
Si no existe, crea el archivo requirements.txt en la raíz del proyecto:
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
pip freeze > requirements.txt (con el entorno virtual activado)
Crea el archivo docker-compose.yml en la raíz del proyecto:
# 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
Crea el archivo nginx.conf en la raíz del proyecto:
# 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;
}
}
}
Para escalamiento avanzado, crea 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
16.6.3. Levantar el sistema con Docker
# 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
Deberías ver 4 contenedores en estado "Up".
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
Nginx balanceará la carga entre las 3 instancias automáticamente.
17. Solución de Problemas Comunes
17.1. Error: "No module named pymysql"
Solución:
# Instalar pymysql pip install pymysql mysqlclient
17.2. Error: "Access denied for user 'root'@'localhost'"
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'"
Solución:
# Crear base de datos CREATE DATABASE biblioteca_dbutres CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; exit;
17.4. Error: "Page not found (404)"
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"
Solución:
En settings.py, asegúrate de tener:
USE_TZ = False TIME_ZONE = 'America/Hermosillo'
17.6. El CSS/JS no se carga
Solución:
Verifica que en settings.py tengas configurado STATIC_URL y STATICFILES_DIRS.
17.7. Error en cliente SOAP: "ConnectionError"
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'"
Solución:
# Instalar zeep pip install zeep
17.9. Docker: "Cannot connect to Docker daemon"
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
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"
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
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
🎯 ¿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 cgiconemail.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
Ruta completa: django-sistema-u3/cliente_soap_visual.py
#!/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)
pip install zeep lxml
1. Asegúrate de que el servidor Django esté corriendo:
python manage.py runserver2. En otra terminal, ejecuta el cliente:
python cliente_soap_visual.py3. 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
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.
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
Ruta completa: django-sistema-u3/fix_spyne_cgi.py
cgi
eliminado en Python 3.13+. Este script parchea Spyne para usar email.message
en su lugar, permitiendo que el servicio SOAP funcione correctamente.
"""
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()
• 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
Ruta completa: django-sistema-u3/EJEMPLO_VISUALIZADOR_SOAP.html
cliente_soap_visual.py. Incluye un ejemplo real
de Request y Response XML con diseño profesional.
<!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"><soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
<soap-env:Body>
<ns0:obtener_libro xmlns:ns0="biblioteca.soap.services">
<ns0:libro_id>1</ns0:libro_id>
</ns0:obtener_libro>
</soap-env:Body>
</soap-env:Envelope></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"><soap11env:Envelope xmlns:soap11env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="biblioteca.soap.services" xmlns:s0="libros.soap_services">
<soap11env:Body>
<tns:obtener_libroResponse>
<tns:obtener_libroResult>
<s0:id>1</s0:id>
<s0:titulo>Cien años de soledad</s0:titulo>
<s0:isbn>9780307474728</s0:isbn>
<s0:numero_paginas>471</s0:numero_paginas>
<s0:idioma>Español</s0:idioma>
<s0:descripcion>Obra maestra del realismo mágico.</s0:descripcion>
<s0:estado>prestado</s0:estado>
<s0:stock_total>5</s0:stock_total>
<s0:stock_disponible>0</s0:stock_disponible>
<s0:autor>
<s0:id>1</s0:id>
<s0:nombre>Gabriel</s0:nombre>
<s0:apellido>García Márquez</s0:apellido>
<s0:nacionalidad>Colombiano</s0:nacionalidad>
<s0:biografia>Premio Nobel de Literatura 1982.</s0:biografia>
</s0:autor>
<s0:editorial>
<s0:id>1</s0:id>
<s0:nombre>Editorial Sudamericana</s0:nombre>
<s0:pais>Argentina</s0:pais>
</s0:editorial>
<s0:categoria>
<s0:id>1</s0:id>
<s0:nombre>Ficción</s0:nombre>
</s0:categoria>
</tns:obtener_libroResult>
</tns:obtener_libroResponse>
</soap11env:Body>
</soap11env:Envelope></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>
cliente_soap_visual.py después de cada operación.
• 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
Ruta completa: django-sistema-u3/biblioteca_project/templates/ejemplos_admin.html
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
Ruta completa: django-sistema-u3/biblioteca_project/templates/ejemplos_rest.html
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
Ruta completa: django-sistema-u3/biblioteca_project/templates/ejemplos_soap.html
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)
19. Funcionalidades Extras y Mejoras
19.1. 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
Presiona Ctrl + C en la terminal donde está corriendo el servidor.
19.3. Desactivar entorno virtual
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
| 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
├── 📁 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
- 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
El proyecto está completamente funcional y listo para usar.