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')