← Volver a Guía Principal

🛠️ CORRECCIONES DESARROLLO LOCAL

VERSIÓN 3.0

Soluciones para problemas comunes en entorno local (localhost)

Universidad Tecnológica de Hermosillo | Profesor: Bernardo Prado

⚠️ 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:

  1. Exporta tus datos (si son importantes)
  2. Elimina la base de datos: DROP DATABASE biblioteca_db;
  3. Vuelve a crearla: CREATE DATABASE biblioteca_db CHARACTER SET utf8 COLLATE utf8_general_ci;
  4. 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

  1. Detección automática de tokens: Si los tokens ya vienen en la URL, los guarda directamente
  2. Limpieza de URL: Elimina los tokens de la barra de direcciones por seguridad
  3. Mejor manejo de errores: Muestra mensajes claros cuando algo falla
  4. 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:

  1. Que todos los servicios estén corriendo (MySQL, Django)
  2. Los logs de Django (python manage.py runserver)
  3. La consola del navegador (F12 → Console) para errores JavaScript
  4. Que las dependencias estén instaladas: pip install -r requirements.txt

Si necesitas desplegar en PythonAnywhere, usa la Guía v4.0