⚠️ CUÁNDO USAR ESTA GUÍA
Aplica estas correcciones si:
- Estás desarrollando en tu computadora local (localhost:8000)
- El login con Google no funciona correctamente
- Los WebSockets no se conectan
- Tienes problemas con la base de datos MySQL
- La API de Google Books no responde
Nota: Si ya desplegaste en PythonAnywhere, usa la Guía v4.0
1CONFIGURACIÓN BASE DE DATOS MySQL
🎯 ¿QUÉ CORRIGE?
Errores relacionados con charset, encoding y tipo de motor de almacenamiento en MySQL que pueden causar:
- Caracteres especiales (ñ, á, é, etc.) que no se guardan correctamente
- Error: "Table doesn't support BLOB/TEXT columns"
- Problemas con transacciones en la base de datos
📝 PASO A PASO
1️⃣ Ubicar el archivo de configuración
Abre el archivo biblioteca_project/settings.py en tu editor de código.
2️⃣ Buscar la sección DATABASES
Localiza esta parte del código (aproximadamente línea 80-95):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'biblioteca_db',
'USER': 'root',
'PASSWORD': 'tu_password',
'HOST': 'localhost',
'PORT': '3306',
}
}
3️⃣ Agregar la sección OPTIONS
Modifica el diccionario para que quede así:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'biblioteca_db',
'USER': 'root',
'PASSWORD': 'tu_password',
'HOST': 'localhost',
'PORT': '3306',
# ⬇️ AGREGAR ESTA SECCIÓN COMPLETA
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES', default_storage_engine=INNODB",
'charset': 'utf8',
'use_unicode': True,
},
}
}
✅ ¿QUÉ HACE CADA PARÁMETRO?
| Parámetro | Función |
|---|---|
default_storage_engine=INNODB |
Fuerza el uso del motor InnoDB que soporta transacciones y claves foráneas |
charset: 'utf8' |
Configura el conjunto de caracteres para soportar acentos y ñ |
use_unicode: True |
Permite usar caracteres Unicode en toda la aplicación |
STRICT_TRANS_TABLES |
Activa modo estricto de MySQL para validaciones más rigurosas |
4️⃣ Guardar y recrear las migraciones
# En tu terminal:
python manage.py makemigrations
python manage.py migrate
⚠️ IMPORTANTE
Si ya tienes datos en tu base de datos, esta configuración no afectará las tablas existentes. Para aplicarla completamente:
- Exporta tus datos (si son importantes)
- Elimina la base de datos:
DROP DATABASE biblioteca_db; - Vuelve a crearla:
CREATE DATABASE biblioteca_db CHARACTER SET utf8 COLLATE utf8_general_ci; - Ejecuta las migraciones nuevamente
2CONFIGURAR OAUTH URIs EN GOOGLE CONSOLE
🎯 ¿QUÉ CORRIGE?
Errores como:
- "redirect_uri_mismatch"
- "Error 400: redirect_uri_mismatch"
- Google no redirige después del login
📝 PASO A PASO
1️⃣ Ir a Google Cloud Console
Accede a https://console.cloud.google.com/apis/credentials
2️⃣ Seleccionar tu proyecto
En la parte superior, asegúrate de tener seleccionado el proyecto correcto (por ejemplo: "Biblioteca UTH")
3️⃣ Editar credenciales OAuth 2.0
1. Busca tu ID de cliente OAuth 2.0
2. Haz clic en el icono del lápiz (editar) ✏️
4️⃣ Agregar URIs de redireccionamiento autorizados
En la sección "URIs de redireccionamiento autorizados", agrega estas DOS URLs:
http://127.0.0.1:8000/oauth/login/
http://localhost:8000/oauth/login/
⚠️ MUY IMPORTANTE
- Agrega AMBAS URLs (con 127.0.0.1 y con localhost)
- Respeta las barras finales / - son obligatorias
- http:// NO https:// (en local usamos http)
- Puerto :8000 debe estar presente
5️⃣ Guardar cambios
Haz clic en el botón "GUARDAR" en la parte inferior de la página.
6️⃣ Verificar en settings.py
Asegúrate de que tu settings.py tenga esta configuración:
SOCIALACCOUNT_PROVIDERS = {
'google': {
'SCOPE': ['profile', 'email'],
'AUTH_PARAMS': {'access_type': 'online'},
'APP': {
'client_id': 'TU_CLIENT_ID.apps.googleusercontent.com',
'secret': 'TU_CLIENT_SECRET',
'key': ''
},
# ⬇️ Importante: estas deben coincidir con Google Console
'REDIRECT_URI': 'http://127.0.0.1:8000/oauth/login/',
}
}
✅ Verificar que funciona
1. Reinicia el servidor Django: python manage.py runserver
2. Ve a http://127.0.0.1:8000/oauth/login/
3. Deberías ver el botón de "Login con Google"
4. Al hacer clic, Google debe redirigir correctamente
3CORRECCIÓN SCRIPT oauth_login.html
🎯 ¿QUÉ CORRIGE?
Problemas en el flujo de autenticación OAuth donde:
- Los tokens no se guardan en localStorage
- La página se queda en "Cargando..."
- No muestra los datos del usuario después del login
- Los tokens aparecen visibles en la URL
📝 PASO A PASO
1️⃣ Ubicar el archivo
Abre templates/oauth_login.html
2️⃣ Localizar la sección <script>
Busca la etiqueta <script> cerca del final del archivo.
3️⃣ REEMPLAZAR TODO el contenido del script
Borra todo lo que esté entre <script> y </script> y pega este código:
<script>
// Leer parámetros de la URL
const urlParams = new URLSearchParams(window.location.search);
// 1. Revisar si ya vienen los TOKENS (éxito del backend)
const accessToken = urlParams.get('access');
const refreshToken = urlParams.get('refresh');
const error = urlParams.get('error');
if (accessToken) {
// ¡LOGIN EXITOSO! El backend ya hizo todo.
// Guardamos tokens
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
// Simulamos el objeto 'data' para reutilizar tu función showSuccess
const userData = {
message: "Login Exitoso",
user: {
email: urlParams.get('email'),
username: urlParams.get('username'),
first_name: '',
last_name: ''
},
google_data: {
picture: urlParams.get('picture')
},
access: accessToken
};
showSuccess(userData);
// Limpiamos la URL para que no se vean los tokens
window.history.replaceState({}, document.title, window.location.pathname);
} else if (error) {
showError(error);
} else {
// Si no hay tokens ni error, iniciamos el proceso pidiendo la URL
initiateGoogleLogin();
}
// Nueva función para iniciar el login
async function initiateGoogleLogin() {
try {
// 1. Pedimos la URL a tu API
const response = await fetch('/api/auth/google/redirect/');
const data = await response.json();
// 2. Usamos la URL que nos dio la API para ir a Google
if (data.auth_url) {
window.location.href = data.auth_url;
} else {
showError("La API no devolvió una URL de autenticación válida.");
}
} catch (err) {
showError("Error conectando con la API de autenticación.");
}
}
async function exchangeCodeForTokens(code) {
try {
const response = await fetch('/api/auth/google/callback/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code: code })
});
const data = await response.json();
if (response.ok) {
// Guardar tokens
localStorage.setItem('access_token', data.access);
localStorage.setItem('refresh_token', data.refresh);
showSuccess(data);
} else {
showError(data.error || 'Error desconocido');
}
} catch (error) {
showError('Error de red: ' + error.message);
}
}
function showSuccess(data) {
document.getElementById('loading').style.display = 'none';
const resultDiv = document.getElementById('result');
resultDiv.className = 'result success';
resultDiv.style.display = 'block';
resultDiv.innerHTML = `
<h2>✅ Login Exitoso</h2>
<div class="user-info">
<p>Bienvenido ${data.user.email}</p>
</div>
<div class="token-display">
Token guardado en localStorage.
</div>
<a href="/" class="btn">🏠 Ir al Inicio</a>
`;
}
function showError(errorMessage) {
document.getElementById('loading').style.display = 'none';
const resultDiv = document.getElementById('result');
resultDiv.className = 'result error';
resultDiv.style.display = 'block';
resultDiv.innerHTML = `<p>${errorMessage}</p>`;
}
</script>
✅ MEJORAS IMPLEMENTADAS
- Detección automática de tokens: Si los tokens ya vienen en la URL, los guarda directamente
- Limpieza de URL: Elimina los tokens de la barra de direcciones por seguridad
- Mejor manejo de errores: Muestra mensajes claros cuando algo falla
- Flujo completo: Inicia el proceso OAuth automáticamente si no hay tokens
4️⃣ Guardar y probar
1. Guarda el archivo
2. Recarga la página http://127.0.0.1:8000/oauth/login/
3. Intenta hacer login con Google
4. Verifica en las DevTools (F12 → Application → Local Storage) que los tokens se guardaron
4IMPLEMENTACIÓN COMPLETA WEBSOCKETS
🎯 ¿QUÉ CORRIGE?
Problemas con WebSockets donde:
- Error: "WebSocket connection failed"
- Las notificaciones en tiempo real no llegan
- El chat no funciona
- Error: "No route found for path 'ws/notificaciones/'"
📝 ARCHIVOS A MODIFICAR/CREAR
📄 1. Modificar asgi.py
Archivo: biblioteca_project/asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'biblioteca_project.settings')
django_asgi_app = get_asgi_application()
from libros.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
)
),
})
📄 2. Crear consumers.py
Archivo: libros/consumers.py (crear si no existe)
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from .models import Libro
class NotificacionesConsumer(AsyncWebsocketConsumer):
"""Consumer para notificaciones en tiempo real"""
async def connect(self):
"""Cuando un cliente se conecta"""
self.room_group_name = 'notificaciones'
# Unirse al grupo
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Mensaje de bienvenida
await self.send(text_data=json.dumps({
'type': 'connection',
'message': '✅ Conectado a notificaciones en tiempo real'
}))
async def disconnect(self, close_code):
"""Cuando un cliente se desconecta"""
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
"""Recibir mensaje del cliente"""
data = json.loads(text_data)
message_type = data.get('type')
if message_type == 'libro_update':
libro_id = data.get('libro_id')
await self.notificar_cambio_libro(libro_id)
async def notificar_cambio_libro(self, libro_id):
"""Notificar a todos sobre cambio en libro"""
libro_data = await self.get_libro_data(libro_id)
# Broadcast a todo el grupo
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'libro_actualizado',
'libro': libro_data
}
)
async def libro_actualizado(self, event):
"""Enviar notificación al cliente"""
await self.send(text_data=json.dumps({
'type': 'libro_actualizado',
'libro': event['libro']
}))
@database_sync_to_async
def get_libro_data(self, libro_id):
"""Obtener datos del libro (sync to async)"""
try:
libro = Libro.objects.get(pk=libro_id)
return {
'id': libro.id,
'titulo': libro.titulo,
'stock': libro.stock,
'disponible': libro.esta_disponible
}
except Libro.DoesNotExist:
return None
class ChatConsumer(AsyncWebsocketConsumer):
"""Consumer para chat de biblioteca"""
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Notificar que alguien se conectó
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_join',
'message': f'Un usuario se unió al chat'
}
)
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
data = json.loads(text_data)
message = data['message']
username = data.get('username', 'Anónimo')
# Enviar mensaje a todos en la sala
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'username': username
}
)
async def chat_message(self, event):
"""Recibir mensaje del grupo y enviarlo al WebSocket"""
await self.send(text_data=json.dumps({
'type': 'message',
'message': event['message'],
'username': event['username']
}))
async def user_join(self, event):
"""Usuario se unió"""
await self.send(text_data=json.dumps({
'type': 'system',
'message': event['message']
}))
📄 3. Crear routing.py
Archivo: libros/routing.py (crear si no existe)
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/notificaciones/$', consumers.NotificacionesConsumer.as_asgi()),
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
4️⃣ Verificar configuración en settings.py
Asegúrate de tener esta configuración en settings.py:
INSTALLED_APPS = [
# ...
'channels',
# ...
]
# Al final del archivo:
ASGI_APPLICATION = 'biblioteca_project.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
}
5️⃣ Probar WebSockets
En tu navegador (DevTools → Console), ejecuta:
// Conectar a notificaciones
const ws = new WebSocket('ws://127.0.0.1:8000/ws/notificaciones/');
ws.onopen = () => console.log('✅ Conectado');
ws.onmessage = (e) => console.log('📩 Mensaje:', JSON.parse(e.data));
ws.onerror = (e) => console.error('❌ Error:', e);
✅ Si todo funciona correctamente
Deberías ver en la consola:
✅ Conectado
📩 Mensaje: {type: "connection", message: "✅ Conectado a notificaciones en tiempo real"}
5CONFIGURACIÓN API GOOGLE BOOKS
🎯 ¿QUÉ CORRIGE?
Problemas al importar libros desde Google Books:
- Error: "API key not configured"
- Error 403: "Daily Limit Exceeded"
- No se pueden buscar ni importar libros
📝 PASO A PASO
1️⃣ Obtener una API Key de Google Books
1. Ve a Google Cloud Console - API Library
2. Busca "Books API"
3. Haz clic en "HABILITAR"
4. Ve a Credenciales → Crear credenciales → Clave de API
5. Copia la API Key generada
2️⃣ Agregar endpoint en api_urls.py
Archivo: libros/api_urls.py
from django.urls import path
from . import api_views
urlpatterns = [
# ... otras rutas ...
# ⬇️ AGREGAR ESTA LÍNEA
path('importar-google/', api_views.importar_desde_google_books, name='books_api'),
]
3️⃣ Crear external_services.py
Archivo: libros/external_services.py (crear si no existe)
import requests
from django.conf import settings
class GoogleBooksAPI:
BASE_URL = 'https://www.googleapis.com/books/v1/volumes'
def __init__(self):
self.api_key = settings.GOOGLE_BOOKS_API_KEY
def buscar_libros(self, query, max_results=10):
"""Buscar libros en Google Books"""
params = {
'q': query,
'maxResults': max_results,
'key': self.api_key
}
try:
response = requests.get(self.BASE_URL, params=params)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error al consultar Google Books API: {e}")
return None
def obtener_libro_por_id(self, book_id):
"""Obtener detalles de un libro específico"""
url = f"{self.BASE_URL}/{book_id}"
params = {'key': self.api_key}
try:
response = requests.get(url, params=params)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error al obtener libro: {e}")
return None
4️⃣ Configurar en settings.py
Agrega al final de settings.py:
# Google Books API
GOOGLE_BOOKS_API_KEY = 'TU_API_KEY_AQUI'
5️⃣ Crear vista en api_views.py
En libros/api_views.py, agrega:
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from .external_services import GoogleBooksAPI
@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated])
def importar_desde_google_books(request):
"""Buscar e importar libros desde Google Books API"""
if request.method == 'GET':
# Buscar libros
query = request.GET.get('q', '')
if not query:
return Response({'error': 'Parámetro "q" requerido'}, status=400)
api = GoogleBooksAPI()
resultados = api.buscar_libros(query)
if resultados:
return Response(resultados)
else:
return Response({'error': 'Error al consultar Google Books'}, status=500)
elif request.method == 'POST':
# Importar libro específico
book_id = request.data.get('book_id')
if not book_id:
return Response({'error': 'book_id requerido'}, status=400)
api = GoogleBooksAPI()
libro_data = api.obtener_libro_por_id(book_id)
if libro_data:
# Aquí procesas y guardas el libro en tu BD
return Response({'mensaje': 'Libro importado', 'data': libro_data})
else:
return Response({'error': 'No se pudo obtener el libro'}, status=500)
6️⃣ Probar la API
Usando Postman o curl:
# Buscar libros
GET http://127.0.0.1:8000/api/libros/importar-google/?q=django
Authorization: Bearer TU_ACCESS_TOKEN
# Respuesta esperada:
{
"kind": "books#volumes",
"items": [...]
}
✅ Verificación exitosa
Si ves un JSON con una lista de libros, la API está funcionando correctamente.
✅ CHECKLIST FINAL
Verifica que aplicaste todas las correcciones necesarias:
| Corrección | Aplicada |
|---|---|
| ✅ Configuración MySQL con InnoDB y UTF-8 | ☐ |
| ✅ URIs de OAuth en Google Console (127.0.0.1 y localhost) | ☐ |
| ✅ Script oauth_login.html actualizado | ☐ |
| ✅ WebSockets: asgi.py, consumers.py, routing.py | ☐ |
| ✅ API de Google Books configurada y funcionando | ☐ |
| ✅ Probado todo en http://127.0.0.1:8000 | ☐ |
⚠️ ¿Aún tienes problemas?
Revisa:
- Que todos los servicios estén corriendo (MySQL, Django)
- Los logs de Django (
python manage.py runserver) - La consola del navegador (F12 → Console) para errores JavaScript
- Que las dependencias estén instaladas:
pip install -r requirements.txt
Si necesitas desplegar en PythonAnywhere, usa la Guía v4.0