Implementación de un Sistema de Foro con Django: Guía Técnica

Configuración Inicial del Proyecto

Para iniciar el sistema de foro, primero establecemos las dependencias esenciales:

django==3.2.12
mysqlclient==2.2.4
pillow==10.2.0
beautifulsoup4==4.12.3
requests==2.31.0
lxml==5.1.0

En el archivo de configuración principal ajustamos parámetros críticos:

# settings.py
BASE_DIR = Path(__file__).resolve().parent.parent

# Configuración de base de datos MySQL
DATABASES = {
    'predeterminada': {
        'MOTOR': 'django.db.backends.mysql',
        'NOMBRE': 'sistema_foro',
        'USUARIO': 'root',
        'CONTRASEÑA': 'tu_contraseña_segura',
        'HOST': '127.0.0.1',
        'PUERTO': '3306',
        'CHARSET': 'utf8mb4'
    }
}

# Ajustes regionales
IDIOMA_PRINCIPAL = 'es-es'
ZONA_HORARIA = 'America/Mexico_City'

# Rutas para recursos estáticos y multimedia
RUTAS_ESTÁTICAS = [BASE_DIR / "recursos_estáticos"]
URL_MEDIO = '/medios/'
RAÍZ_MEDIO = BASE_DIR / 'medios'

# Modelo personalizado de usuario
MODELO_USUARIO = 'usuarios.Perfil'

Estructura de Modelos de Datos

El esquema de base de datos se organiza mediante relaciones especializadas:

Entidad Atributos Clave Relaciones
Perfil teléfono, avatar, fecha_creación Uno a uno con SitioPersonal
SitioPersonal nombre_sitio, título, tema Uno a muchos con Artículo/Categoría
Artículo título, resumen, contenido, fecha_publicación Uno a muchos con SitioPersonal/Categoría, Muchos a muchos con Etiqueta
Voto es_positivo Relación usuario-artículo
Comentario contenido, fecha_comentario Auto-relación para respuestas

Implementamos un modelo base abstracto para campos comunes:

class ModeloBase(models.Model):
    fecha_creacion = models.DateTimeField(auto_now_add=True)
    ultima_actualizacion = models.DateTimeField(auto_now=True)

    class Meta:
        abstracto = True  # Evita creación física en BD

class Perfil(AbstractUser, ModeloBase):
    telefono = models.BigIntegerField(null=True)
    avatar = models.ImageField(upload_to='avatares/')
    sitio = models.OneToOneField('SitioPersonal', on_delete=models.CASCADE)

    class Meta:
        nombre_tabla = 'perfiles_usuarios'

Servicios Comunes del Sistema

Implementamos utilidades para respuestas estandarizadas:

from django.http import JsonResponse

def respuesta_estandar(codigo=200, mensaje=None, **extras):
    datos = {'codigo': codigo}
    if mensaje:
        datos['mensaje'] = mensaje
    datos.update(extras)
    return JsonResponse(datos)

Componente de paginación con lógica mejorada:

class Paginador:
    def __init__(self, pagina_actual, total_elementos, por_pagina=10, max_botones=7):
        self.pagina = max(1, int(pagina_actual))
        self.total = total_elementos
        self.elementos_por_pagina = por_pagina
        self.total_paginas = (total_elementos + por_pagina - 1) // por_pagina
        self.pagina = min(self.pagina, self.total_paginas)
        self.max_botones = max_botones

    @property
    def inicio(self):
        return (self.pagina - 1) * self.elementos_por_pagina

    def generar_html(self):
        html = ['<ul class="paginacion">']
        # Lógica para botones de navegación
        # ... implementación optimizada ...
        html.append('</ul>')
        return ''.join(html)

Gestión de Usaurios

El registro de usuarios integra la creación del sitio personnel:

def registro_usuario(solicitud):
    if solicitud.method == 'POST' and solicitud.is_ajax():
        formulario = FormularioRegistro(solicitud.POST, solicitud.FILES)
        if not formulario.is_valid():
            return respuesta_estandar(400, errores=formulario.errors)
        
        datos = formulario.limpiar_datos()
        sitio = SitioPersonal.objects.crear(
            nombre=datos['nombre_usuario'],
            titulo=datos.pop('titulo_sitio')
        )
        
        Perfil.objects.crear_usuario(
            sitio=sitio,
            avatar=solicitud.FILES.get('avatar'),
            **datos
        )
        return respuesta_estandar(200, f"¡{datos['nombre_usuario']} registrado!")
    return render(solicitud, 'registro.html')

El cambio de contraseña incluye verificación de seguridad:

def actualizar_contraseña(solicitud):
    if solicitud.is_ajax() and solicitud.method == 'POST':
        antigua = solicitud.POST.get('contraseña_anterior')
        nueva = solicitud.POST.get('contraseña_nueva')
        captcha = solicitud.POST.get('captcha')
        
        if captcha.upper() != solicitud.session.get('captcha').upper():
            return respuesta_estandar(401, "Código de verificación inválido")
        
        if not solicitud.user.check_password(antigua):
            return respuesta_estandar(403, "Contraseña actual incorrecta")
        
        if len(nueva) < 6:
            return respuesta_estandar(400, "Mínimo 6 caracteres")
        
        solicitud.user.set_password(nueva)
        solicitud.user.save()
        return respuesta_estandar(200, "Contraseña actualizada")

Funcionalidades de Contenido

El sistema de votos implementa lógica anti-abuso:

def procesar_voto(solicitud):
    usuario = solicitud.user
    articulo_id = int(solicitud.POST.get('articulo_id'))
    es_positivo = solicitud.POST.get('tipo') == 'positivo'

    articulo = Artículo.objects.get(id=articulo_id)
    if articulo.autor == usuario:
        return respuesta_estandar(403, "No puedes votar tu propio contenido")

    voto_existente = Voto.objects.filter(
        usuario=usuario, 
        articulo=articulo
    ).first()

    if voto_existente:
        if voto_existente.es_positivo == es_positivo:
            return respuesta_estandar(400, "Voto duplicado no permitido")
        # Inversión de voto
        articulo.actualizar_votos(es_positivo, invertir=True)
        voto_existente.es_positivo = es_positivo
        voto_existente.save()
    else:
        Voto.crear(usuario, articulo, es_positivo)
        articulo.actualizar_votos(es_positivo)
    
    return respuesta_estandar(200, "Voto registrado")

El sistema de comentarios maneja respuestas anidadas:

def cargar_comentarios(articulo):
    comentarios_principales = Comentario.objects.filter(
        articulo=articulo, 
        padre=None
    )
    estructura = []
    for comentario in comentarios_principales:
        respuestas = Comentario.objects.filter(padre=comentario)
        estructura.append((comentario, respuestas))
    return estructura

Interfaz de Administración

Implementamos pestañas dinámicas para el panel de control:

<div class="panel-control">
  <ul class="pestanas" role="tablist">
    <li class="activa"><a href="#articulos" data-toggle="tab">Mis Artículos</a></li>
    <li><a href="#papelera" data-toggle="tab">Papelera</a></li>
  </ul>

  <div class="contenido-pestana">
    <div id="articulos" class="tab-pane activo">
      <!-- Listado de artículos -->
    </div>
    <div id="papelera" class="tab-pane">
      <!-- Elementos eliminados -->
    </div>
  </div>
</div>

La funcionalidad de papelera utiliza marcado lógico:

def papelera(solicitud):
    articulos_eliminados = Artículo.objects.filter(
        autor=solicitud.user,
        eliminado=True
    )
    return render(solicitud, 'papelera.html', {'articulos': articulos_eliminados})

def restaurar_articulo(solicitud, articulo_id):
    Artículo.objects.filter(id=articulo_id).update(eliminado=False)
    return redirect('papelera')

Etiquetas: Django MySQL Bootstrap AJAX Python

Publicado el 8-27 03:34