Skip to content

Guía de estilo de programación

Agustín Figueroa Sierra edited this page Sep 6, 2025 · 1 revision

Guía de Estilo de Programación - ComercIA

�� Principios Generales

Filosofía de Código

  • Legibilidad sobre concisión: El código debe ser fácil de leer y entender
  • Consistencia: Mantener el mismo estilo en todo el proyecto
  • Documentación: Código autodocumentado con comentarios cuando sea necesario
  • Mantenibilidad: Estructura que facilite futuras modificaciones

🐍 Python / Django

Convenciones de Nomenclatura

# ✅ CORRECTO
class Product(models.Model):
    name = models.CharField(max_length=255, verbose_name='Nombre')
    
def get_user_recommendations(user):
    """Obtiene recomendaciones personalizadas para un usuario"""
    pass

# ❌ INCORRECTO
class prod(models.Model):  # Abreviación
    n = models.CharField(max_length=255)  # Nombre no descriptivo

def get_rec(u):  # Abreviación y parámetro no descriptivo
    pass

Estructura de Archivos

app_name/
├── models.py          # Modelos de datos
├── views.py           # Vistas y lógica de presentación
├── forms.py           # Formularios Django
├── admin.py           # Configuración del admin
├── urls.py            # URLs de la aplicación
├── services.py        # Lógica de negocio (si existe)
├── utils.py           # Utilidades y helpers
└── templates/         # Templates HTML
    └── app_name/

Modelos Django

# ✅ CORRECTO - Estructura clara y documentada
class Product(models.Model):
    """Modelo que representa un producto en el catálogo"""
    
    CATEGORY_CHOICES = [
        ('Comida', 'Comida'),
        ('Ropa', 'Ropa'),
        ('Tecnología', 'Tecnología'),
    ]
    
    name = models.CharField(
        max_length=255,
        verbose_name='Nombre',
        help_text='Nombre del producto'
    )
    
    def clean(self):
        """Validación personalizada del modelo"""
        if self.category == 'Comida':
            self.condition = None
        super().clean()
    
    def __str__(self):
        return f"{self.name} - {self.get_category_display()}"
    
    class Meta:
        verbose_name = 'Producto'
        verbose_name_plural = 'Productos'
        ordering = ['-published_at']

Vistas Django

# ✅ CORRECTO - Vista limpia y bien estructurada
@login_required
def add_product(request):
    """Vista para agregar un nuevo producto"""
    if request.method == 'POST':
        form = ProductForm(request.POST, request.FILES)
        if form.is_valid():
            product = form.save(commit=False)
            product.seller = request.user
            product.save()
            messages.success(request, '¡Producto agregado exitosamente!')
            return redirect('home')
        else:
            messages.error(request, 'Por favor corrige los errores en el formulario.')
    else:
        form = ProductForm()
    
    return render(request, 'products/add_product.html', {'form': form})

# ❌ INCORRECTO - Vista "gorda" con mucha lógica
def add_product(request):
    # 200+ líneas de lógica de negocio mezclada
    pass

Formularios Django

# ✅ CORRECTO - Formulario bien estructurado
class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ['name', 'category', 'description', 'price', 'image']
        widgets = {
            'name': forms.TextInput(attrs={'class': 'form-control'}),
            'description': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 4,
                'placeholder': 'Describe tu producto detalladamente'
            }),
        }
    
    def clean(self):
        """Validación personalizada del formulario"""
        cleaned_data = super().clean()
        category = cleaned_data.get('category')
        
        if category == 'Comida':
            food_type = cleaned_data.get('food_type')
            if not food_type:
                self.add_error('food_type', 'Debe seleccionar un tipo de comida')
        
        return cleaned_data

Manejo de Errores

# ✅ CORRECTO - Manejo explícito de errores
try:
    processor = GeminiProcessor()
    result = processor.process_query(query)
    if result.get('success', False):
        # Procesar resultado exitoso
        pass
    else:
        messages.error(request, f"Error: {result.get('error', 'Error desconocido')}")
except Exception as e:
    logger.error(f"Error procesando consulta: {str(e)}")
    messages.error(request, 'Error interno del servidor')

# ❌ INCORRECTO - Ignorar errores
processor = GeminiProcessor()
result = processor.process_query(query)  # Sin manejo de errores

🎨 Templates HTML

Estructura de Templates

<!-- ✅ CORRECTO - Extensión del template base -->
{% extends 'products/base.html' %}
{% load humanize %}

{% block title %}ComercIA - Inicio{% endblock %}

{% block extra_styles %}
<style>
    .product-card {
        height: 100%;
        transition: all 0.2s;
        border: none;
        box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    }
</style>
{% endblock %}

{% block content %}
<div class="container py-4">
    <!-- Contenido principal -->
</div>
{% endblock %}

Uso de Variables y Filtros

<!-- ✅ CORRECTO - Uso apropiado de filtros -->
<h5 class="card-title">{{ product.name|truncatechars:50 }}</h5>
<p class="price">$ {{ product.price|floatformat:0|intcomma }}</p>
<span class="badge bg-primary">{{ product.get_category_display }}</span>

<!-- ❌ INCORRECTO - Lógica compleja en template -->
{% if product.name|length > 50 %}
    {{ product.name|slice:":50" }}...
{% else %}
    {{ product.name }}
{% endif %}

Formularios en Templates

<!-- ✅ CORRECTO - Formulario bien estructurado -->
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <div class="mb-3">
        <label for="{{ form.name.id_for_label }}" class="form-label">
            {{ form.name.label }}
        </label>
        {{ form.name }}
        {% if form.name.errors %}
            <div class="text-danger">
                {% for error in form.name.errors %}
                    <small>{{ error }}</small>
                {% endfor %}
            </div>
        {% endif %}
    </div>
    <button type="submit" class="btn btn-primary">Guardar</button>
</form>

🎯 JavaScript

Estructura y Nomenclatura

// ✅ CORRECTO - Función bien documentada
function toggleFavorite(event, productId) {
    /**
     * Alterna el estado de favorito de un producto
     * @param {Event} event - Evento del click
     * @param {number} productId - ID del producto
     */
    if (event) event.preventDefault();
    
    fetch(`/favorite/toggle/${productId}/`, {
        method: 'POST',
        headers: {
            'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
            'Content-Type': 'application/json'
        }
    })
    .then(response => response.json())
    .then(data => {
        updateFavoriteButton(productId, data.status);
    })
    .catch(error => {
        console.error('Error:', error);
        showErrorMessage('Error al actualizar favoritos');
    });
}

// ❌ INCORRECTO - Función sin documentar y mal nombrada
function toggle(e, id) {
    // Sin documentación y nombres no descriptivos
}

Manejo de Eventos

// ✅ CORRECTO - Delegación de eventos
document.addEventListener('DOMContentLoaded', function() {
    // Delegar clicks de botones de favoritos
    document.addEventListener('click', function(e) {
        const btn = e.target.closest('.favorite-toggle');
        if (!btn) return;
        
        const productId = btn.getAttribute('data-product-id');
        if (productId) {
            toggleFavorite(e, productId);
        }
    });
});

🎨 CSS

Organización y Nomenclatura

/* ✅ CORRECTO - Uso de variables CSS y nomenclatura BEM */
:root {
    --primary-color: hwb(216 4% 67%);
    --secondary-color: #6c757d;
    --card-shadow: 0 10px 20px rgba(0,0,0,0.05);
    --border-radius: 16px;
    --transition-speed: 0.3s;
}

.product-card {
    height: 100%;
    transition: all var(--transition-speed);
    border: none;
    box-shadow: var(--card-shadow);
}

.product-card:hover {
    transform: translateY(-3px);
    box-shadow: var(--hover-shadow);
}

.product-card__image {
    height: 160px;
    object-fit: cover;
}

.product-card__title {
    font-size: 0.9rem;
    font-weight: 500;
    line-height: 1.2;
}

/* ❌ INCORRECTO - Nombres no descriptivos y sin organización */
.card1 {
    height: 100%;
}

.img {
    height: 160px;
}

🔧 Configuración y Settings

Variables de Entorno

# ✅ CORRECTO - Configuración clara y documentada
# X API v2 configuration
X_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAA..."  # Obtener de https://developer.twitter.com
X_USER_ID = "1963212843915923456"  # ID del usuario a analizar
X_USERNAME = "testingAPIs01"  # Username sin @
X_MAX_RESULTS = 3  # Limitar cantidad de tweets por petición

# ❌ INCORRECTO - Configuración sin documentar
X_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAA..."
X_USER_ID = "1963212843915923456"

📝 Documentación

Docstrings

# ✅ CORRECTO - Docstring completo
def recommend_categories_from_text(text: str, keyword_map: dict | None = None) -> list[str]:
    """
    Recomienda categorías de productos basadas en el texto de entrada.
    
    Args:
        text (str): Texto a analizar para detectar categorías
        keyword_map (dict, optional): Mapeo personalizado de palabras clave a categorías.
                                    Si no se proporciona, usa el mapeo por defecto.
    
    Returns:
        list[str]: Lista de categorías detectadas, ordenadas alfabéticamente
    
    Example:
        >>> recommend_categories_from_text("Me gusta la comida como el mango")
        ['Comida']
    """
    if not text:
        return []
    
    mapping = keyword_map or DEFAULT_KEYWORD_MAP
    lowered = text.lower()
    categories: set[str] = set()
    
    for keyword, category in mapping.items():
        if keyword in lowered:
            categories.add(category)
    
    return sorted(categories)

Comentarios en Código

# ✅ CORRECTO - Comentarios útiles y concisos
# Configuración para la zona horaria de Colombia (UTC-5)
COLOMBIA_TIMEZONE = pytz.timezone('America/Bogota')

# Si es un nuevo comentario, asignar la fecha actual con la zona horaria de Colombia
if not self.pk:
    now_utc = timezone.now()
    self.created_at = now_utc.astimezone(COLOMBIA_TIMEZONE)

# ❌ INCORRECTO - Comentarios obvios
# Asignar valor a variable
x = 5

# Incrementar contador
counter += 1

�� Git y Control de Versiones

Mensajes de Commit

# ✅ CORRECTO - Mensajes descriptivos y estructurados
git commit -m "feat: agregar búsqueda inteligente con Gemini AI

- Implementar GeminiProcessor para procesamiento de lenguaje natural
- Agregar endpoint /chat/search/ para consultas AJAX
- Crear modelo ChatQuery para historial de consultas
- Actualizar template home.html con interfaz de chatbot"

git commit -m "fix: corregir validación de formulario de productos

- Arreglar validación de food_type cuando category es 'Comida'
- Mejorar mensajes de error en ProductForm.clean()"

git commit -m "docs: actualizar guía de estilo de programación

- Agregar ejemplos de código para Python/Django
- Incluir convenciones para JavaScript y CSS
- Documentar patrones de manejo de errores"

# ❌ INCORRECTO - Mensajes vagos
git commit -m "fix"
git commit -m "update"
git commit -m "changes"

Estructura de Branches

# ✅ CORRECTO - Nomenclatura clara de branches
feature/gemini-ai-integration
feature/telegram-support
bugfix/product-form-validation
hotfix/security-patch
docs/style-guide-update

# ❌ INCORRECTO - Nombres confusos
branch1
test
fix
new-feature

📊 Testing y Calidad

Estructura de Tests

# ✅ CORRECTO - Test bien estructurado
class ProductModelTest(TestCase):
    """Tests para el modelo Product"""
    
    def setUp(self):
        """Configuración inicial para cada test"""
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123'
        )
    
    def test_product_creation(self):
        """Test que verifica la creación correcta de un producto"""
        product = Product.objects.create(
            name='Producto de prueba',
            category='Comida',
            food_type='Snacks',
            description='Descripción del producto',
            price=1000.00,
            seller=self.user
        )
        
        self.assertEqual(product.name, 'Producto de prueba')
        self.assertEqual(product.category, 'Comida')
        self.assertTrue(product.available)
    
    def test_product_clean_method(self):
        """Test que verifica la validación del método clean"""
        product = Product(
            name='Producto de prueba',
            category='Comida',
            condition='Nuevo',  # No debería aplicarse para comida
            description='Descripción',
            price=1000.00,
            seller=self.user
        )
        
        product.clean()
        self.assertIsNone(product.condition)

🚀 Despliegue y Producción

Configuración de Entornos

# ✅ CORRECTO - Configuración por entornos
DEBUG = 'RENDER' not in os.environ

# Permitir conexiones desde otros dispositivos en la red local y ngrok
ALLOWED_HOSTS = []
RENDER_EXTERNAL_HOSTNAME = os.environ.get('RENDER_EXTERNAL_HOSTNAME')
if RENDER_EXTERNAL_HOSTNAME:
    ALLOWED_HOSTS.append(RENDER_EXTERNAL_HOSTNAME)

# Configuración CSRF para ngrok
CSRF_TRUSTED_ORIGINS = [
    'https://*.ngrok-free.app',
    'https://*.ngrok.io',
    'https://*.ngrok.app',
]

📋 Checklist de Revisión

Antes de hacer Commit

  • El código sigue las convenciones de nomenclatura
  • Las funciones tienen docstrings apropiados
  • Se manejan los errores correctamente
  • Los templates extienden el base.html
  • Los formularios tienen validación apropiada
  • El CSS usa variables y nomenclatura consistente
  • Los mensajes de commit son descriptivos
  • No hay código comentado innecesario
  • Las importaciones están organizadas

Antes de hacer Pull Request

  • El código ha sido probado localmente
  • Se han ejecutado los tests existentes
  • La documentación está actualizada
  • El PR tiene una descripción clara
  • Se han revisado los cambios de migración
  • No hay conflictos con la rama principal

🎯 Herramientas Recomendadas

Linting y Formateo

# Instalar herramientas de calidad de código
pip install flake8 black isort

# Formatear código automáticamente
black .
isort .

# Verificar estilo de código
flake8 .

Pre-commit Hooks

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/psf/black
    rev: 22.3.0
    hooks:
      - id: black
  - repo: https://github.com/pycqa/isort
    rev: 5.10.1
    hooks:
      - id: isort
  - repo: https://github.com/pycqa/flake8
    rev: 4.0.1
    hooks:
      - id: flake8

🎨 Convenciones Específicas de ComercIA

Estructura de Proyecto

comercIA/
├── products/              # App principal de productos
├── seller_profiles/       # App de perfiles de vendedores
├── social_ingestion/      # App de ingesta de redes sociales
├── comercia/              # Configuración del proyecto
├── media/                 # Archivos multimedia
├── staticfiles/           # Archivos estáticos
└── docs/                  # Documentación

Patrones de IA

# ✅ CORRECTO - Procesador de IA bien estructurado
class GeminiProcessor:
    def __init__(self):
        self.api_key = os.getenv('GEMINI_API_KEY')
        self.model_name = "gemini-1.5-flash"
        self.api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model_name}:generateContent"
    
    def process_query(self, query):
        """Procesa consulta en lenguaje natural y extrae palabras clave"""
        try:
            # Lógica de procesamiento
            pass
        except Exception as e:
            return {"success": False, "error": str(e)}

Integración con APIs Externas

# ✅ CORRECTO - Manejo de APIs con rate limiting
def _fetch_x_for_account(self, account: SocialAccount) -> list[dict[str, Any]]:
    bearer_token = getattr(settings, "X_BEARER_TOKEN", "").strip()
    if not bearer_token or bearer_token == "TU_TOKEN_DE_ACCESO_AQUI":
        self.stdout.write(self.style.WARNING("X_BEARER_TOKEN no configurado."))
        return []
    
    # Lógica de fetch con manejo de rate limits
    backoff_seconds = 5
    for attempt in range(3):
        try:
            response = requests.get(url, headers=headers, params=params, timeout=15)
            if response.status_code == 200:
                return self._parse_response(response)
            elif response.status_code == 429:
                self.stdout.write(self.style.WARNING(f"Rate limit X (429). Reintentando en {backoff_seconds}s..."))
                time.sleep(backoff_seconds)
                backoff_seconds *= 2
                continue
        except requests.exceptions.RequestException as e:
            self.stdout.write(self.style.ERROR(f"X API request failed: {e}"))
            break
    
    return []

Esta guía debe ser seguida por todos los desarrolladores del proyecto ComercIA para mantener la consistencia y calidad del código.

Clone this wiki locally