Plantilla de ejecución · Cero conocimientos previos · Versiones 2026

Guía Paso a Paso: de una pantalla en blanco a tu app publicada en internet

Esta guía asume que nunca has programado. Cada paso trae exactamente qué escribir, dónde hacer clic y qué debe aparecer en tu pantalla si vas bien. Sigue el orden: Preparación → Unidad 1 → Unidad 2 → Unidad 3 → Unidad 4.

Cómo leer esta guía — significado de cada color

💡 Tip — un atajo o dato que facilita el paso.
⚠️ Cuidado — error común, revisa dos veces.
🚫 Importante — si te saltas esto, nada más funciona.
Punto de control — así debe verse tu pantalla si vas bien.
>_ Terminal — cópialo tal cual en la terminal de VS Code.
{ } Código — cada línea trae su propio comentario explicando qué hace.
Versiones de referencia 2026 — instala siempre "la más reciente estable" de cada una
Python3.12 o 3.13 (instalador oficial en python.org)
Django5.1.x o superior (lo instala pip automáticamente en su versión estable más nueva)
pymysql1.1.x
python-decouple3.8.x
gunicorn / whitenoise23.x / 6.8.x
MySQL (Aiven)8.4 LTS (el que Aiven asigna por defecto)
Git / VS CodeÚltimas versiones estables (se actualizan solas)
💡 Tip: nunca escribas un número de versión "a mano" al instalar — deja que el instalador oficial te dé la más reciente. Esta guía funciona igual aunque el número suba con el tiempo.
Preparación

Antes de la Unidad 1: prepara tu computadora

Esto se hace una sola vez, al inicio del curso. Sigue el orden exacto.

GitHub es donde vas a guardar tu código en internet, como una "nube" para programadores. Ve al buscador, escribe github.com, botón "Sign up", usa tu correo, elige un usuario que se vea profesional (evita apodos), confirma tu correo.

🚫 Importante: usa el mismo usuario de GitHub durante todo el curso. Vas a compartir enlaces de tu perfil como evidencia.

Git es el programa que "guarda versiones" de tu código y las manda a GitHub. Descárgalo desde git-scm.com (botón grande "Download"). Instálalo dejando todas las opciones por defecto (clic en "Next" todo el tiempo).

Para comprobar que quedó instalado, abre VS Code (lo instalas en el paso 0.4) o cualquier terminal y escribe:

git --version
# Este comando le pregunta a la computadora "¿tienes Git instalado y qué versión?"
✅ Si ves algo como git version 2.4x.x, quedó instalado correctamente.

Git necesita saber "quién eres" para firmar tus entregas (commits). Se hace una sola vez por computadora:

git config --global user.name "Tu Nombre Completo"
# Guarda tu nombre; aparecerá junto a cada entrega que hagas

git config --global user.email "tu-correo@ejemplo.com"
# Guarda tu correo; debe ser el mismo con el que te registraste en GitHub

Es el "cuaderno" donde vas a escribir todo tu código. Descárgalo desde code.visualstudio.com (botón "Download"), instálalo con las opciones por defecto.

Una vez abierto, instala estas 3 extensiones (ícono de cuadritos en la barra izquierda, buscar por nombre e "Install"):

  • Python (de Microsoft) — te ayuda a escribir Python sin errores de sintaxis.
  • Tailwind CSS IntelliSense — te sugiere las clases de Tailwind mientras escribes.
  • Live Server — abre tu HTML en el navegador y lo actualiza solo cuando guardas.

Descárgalo desde python.org → "Downloads". En Windows, en la primera pantalla del instalador marca la casilla "Add python.exe to PATH" antes de darle "Install Now" — es el error número uno de quienes empiezan.

⚠️ Cuidado: si olvidaste marcar esa casilla, desinstala Python y vuelve a instalarlo marcándola. Es más rápido que arreglarlo después.

Verifica abriendo una terminal nueva:

python --version
# Debe responder algo como "Python 3.12.x" o "Python 3.13.x"

Crea en tu computadora una carpeta llamada aplicaciones-web-curso (sin espacios, sin acentos). Ahí vivirán las 4 Unidades. Ábrela en VS Code: menú File → Open Folder…

💡 Tip: la terminal integrada de VS Code se abre con el menú Terminal → New Terminal. Ahí vas a escribir el 90% de los comandos de esta guía.
Unidad 1

Front-End Esencial: tu primera página, publicada

Al terminar esta Unidad vas a tener una página web tuya, visible desde cualquier celular del mundo, con una dirección propia.

Dentro de tu carpeta, crea una subcarpeta unidad-1 y adentro un archivo llamado exactamente index.html (clic derecho en el explorador de VS Code → "New File").

🚫 Importante: el nombre debe ser exactamente index.html, en minúsculas. Los navegadores y Render buscan ese nombre por defecto.

Copia exactamente este código dentro de index.html. Cada línea trae su comentario explicando para qué sirve:

<!DOCTYPE html> <!-- Le dice al navegador "esto es HTML5 moderno" -->
<html lang="es"> <!-- Abre el documento; lang="es" ayuda a lectores de pantalla a leerlo en español -->
<head> <!-- Aquí va todo lo que NO se ve directamente en la página (configuración) -->
  <meta charset="UTF-8" /> <!-- Permite usar tildes y la ñ sin que se vean rotas -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Hace que se vea bien en celular -->
  <title>Mi Portafolio</title> <!-- Texto que aparece en la pestaña del navegador -->
</head>
<body> <!-- Aquí empieza todo lo que SÍ se ve en la página -->

  <header> <!-- Encabezado: se repite igual en todas las páginas de un sitio -->
    <h1>Tu Nombre Completo</h1> <!-- Título principal, debe haber solo UNO por página -->
    <p>Desarrollador Web en formación</p> <!-- Subtítulo o frase corta de presentación -->
  </header>

  <nav> <!-- Barra de navegación: los enlaces para moverte dentro de la página -->
    <a href="#sobre-mi">Sobre mí</a> <!-- El # significa "salta a la sección con ese id" -->
    <a href="#proyectos">Proyectos</a>
    <a href="#contacto">Contacto</a>
  </nav>

  <main> <!-- Contiene el contenido ÚNICO de esta página (no se repite en otras) -->

    <section id="sobre-mi"> <!-- Un bloque temático de contenido; el id lo conecta con el <nav> de arriba -->
      <h2>Sobre mí</h2>
      <p>Escribe aquí 2-3 líneas sobre ti.</p>
    </section>

    <section id="proyectos">
      <h2>Proyectos</h2>
      <article> <!-- Un artículo: contenido que tendría sentido por sí solo, como una tarjeta de proyecto -->
        <h3>Nombre del proyecto</h3>
        <p>Breve descripción de qué hace.</p>
      </article>
    </section>

    <section id="contacto">
      <h2>Contacto</h2>
      <form> <!-- Formulario: donde el usuario puede escribir y enviar datos -->
        <label for="correo">Tu correo:</label> <!-- El for="correo" conecta esta etiqueta con el input de abajo -->
        <input type="email" id="correo" name="correo" required /> <!-- required = campo obligatorio -->
        <button type="submit">Enviar</button> <!-- Botón que envía el formulario -->
      </form>
    </section>

  </main>

  <footer> <!-- Pie de página: información legal, redes sociales, copyright -->
    <p>&copy; 2026 Tu Nombre. Todos los derechos reservados.</p>
  </footer>

</body>
</html>

Guarda con Ctrl+S. Clic derecho sobre el archivo → "Open with Live Server" para verlo en tu navegador.

✅ Debes ver texto simple, sin colores ni diseño todavía — eso es normal, el diseño llega en el Paso 1.4 con Tailwind.

En GitHub.com, botón verde "New" → nombre del repositorio: mi-portafolio → "Public" → "Create repository". GitHub te mostrará unos comandos; en la terminal de VS Code, dentro de unidad-1, escribe:

git init
# Convierte esta carpeta en un repositorio: a partir de aquí Git empieza a "vigilar" los cambios

git add index.html
# Marca el archivo index.html como "listo para guardar" (staging)

git commit -m "Primera version de mi portafolio"
# Guarda una "fotografía" de tu código con un mensaje que describe qué hiciste

git branch -M main
# Nombra tu rama principal "main" (el estándar actual de GitHub)

git remote add origin https://github.com/TU-USUARIO/mi-portafolio.git
# Le dice a Git dónde vive la copia en internet (reemplaza TU-USUARIO por tu usuario real)

git push -u origin main
# Sube tu fotografía guardada (commit) desde tu computadora hacia GitHub
⚠️ Cuidado: la primera vez que haces push, VS Code o la terminal te pedirán iniciar sesión en GitHub desde el navegador. Es normal, acepta el permiso.

Dentro de <head>, justo antes de </head>, agrega esta línea:

<script src="https://cdn.tailwindcss.com"></script>
<!-- Descarga Tailwind desde internet cada vez que se abre la página; con esto ya puedes usar sus clases -->

Ahora agrega clases de Tailwind a tus etiquetas existentes (sin cambiar la estructura). Ejemplo del header:

<header class="bg-indigo-700 text-white text-center py-10 px-4">
  <!-- bg-indigo-700 = fondo morado-azulado · text-white = letras blancas -->
  <!-- text-center = texto centrado · py-10 = relleno arriba/abajo · px-4 = relleno izq/der -->
  <h1 class="text-3xl font-bold">Tu Nombre Completo</h1>
  <!-- text-3xl = letra grande · font-bold = negritas -->
  <p class="mt-2 text-indigo-200">Desarrollador Web en formación</p>
  <!-- mt-2 = margen arriba pequeño · text-indigo-200 = morado clarito -->
</header>
💡 Tip: el nombre de cada clase describe lo que hace: bg- = fondo (background), text- = texto, p/m = padding/margin, el número (100 a 900) = qué tan oscuro es el color.

Usa el prefijo md: para decir "esta clase solo aplica en pantallas medianas en adelante". Ejemplo en <nav>:

<nav class="flex flex-col md:flex-row gap-2 md:gap-6 justify-center py-4 bg-slate-100">
  <!-- flex-col = enlaces apilados en columna (celular) -->
  <!-- md:flex-row = a partir de pantalla mediana, en fila (computadora) -->
  <!-- gap-2 / md:gap-6 = espacio entre enlaces, más grande en pantallas grandes -->
  <a href="#sobre-mi" class="text-indigo-700 font-semibold hover:underline">Sobre mí</a>
  <!-- hover:underline = subraya el enlace SOLO cuando el mouse pasa encima -->
</nav>

Guarda, mira Live Server, y achica la ventana del navegador o abre las DevTools (F12 → ícono de celular) para probar en modo móvil.

git add index.html
# Marca el archivo modificado como listo para guardar

git commit -m "Agrego Tailwind y diseno responsivo"
# Guarda la nueva fotografia con un mensaje describiendo el cambio

git push
# Sube el cambio a GitHub (ya no hace falta -u origin main, Git ya lo recuerda)
  1. Ve a render.com → "Get Started" → elige registrarte con tu cuenta de GitHub (no pide tarjeta).
  2. Botón "New +" → "Static Site".
  3. Selecciona el repositorio mi-portafolio (autoriza a Render a verlo si te lo pide).
  4. En "Publish Directory" deja vacío o escribe . (un punto, significa "la raíz de la carpeta").
  5. Botón "Create Static Site". Espera 1-2 minutos mientras Render construye tu sitio.
✅ Cuando termine, Render te da una URL parecida a mi-portafolio.onrender.com. Ábrela: debe verse igual que en tu Live Server.

A partir de ahora, cada vez que hagas git push, Render se entera solo y vuelve a publicar tu sitio en 1-2 minutos. No necesitas volver a configurar nada.

💡 Tip: guarda la URL de Render en un lugar seguro (notas del celular, por ejemplo) — la necesitarás para tu plantilla de portafolio y para cada entrega.
Unidad 2

Django + MySQL en la nube: tu página ahora "piensa"

Pasas de HTML fijo a una aplicación que guarda y muestra información real desde una base de datos.

Un entorno virtual es una "caja aislada" para instalar librerías de Python sin mezclarlas con otros proyectos. Dentro de tu carpeta unidad-2:

python -m venv venv
# Crea una carpeta llamada "venv" con una copia aislada de Python para este proyecto

Actívalo (el comando cambia según tu sistema operativo):

# En Windows (PowerShell):
venv\Scripts\Activate.ps1

# En Mac / Linux:
source venv/bin/activate
✅ Si funcionó, tu terminal ahora muestra (venv) al inicio de la línea.
pip install django pymysql python-decouple
# pip = instalador de paquetes de Python
# django = el framework que vamos a usar
# pymysql = permite a Django "hablar" con MySQL
# python-decouple = lee variables secretas desde un archivo .env
django-admin startproject impulsatec .
# Crea la estructura base de Django; el "." final = "aquí mismo, no en una subcarpeta nueva"

python manage.py startapp catalogo
# Crea una "app" (módulo) llamada catalogo, donde vivirá el modelo del proyecto integrador

Abre impulsatec/settings.py y busca la lista INSTALLED_APPS, agrega tu app al final:

INSTALLED_APPS = [
    'django.contrib.admin',       # Panel de administración incluido en Django
    'django.contrib.auth',        # Sistema de usuarios y contraseñas incluido
    'django.contrib.contenttypes',
    'django.contrib.sessions',    # Maneja las sesiones (recordar que iniciaste sesión)
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'catalogo',                   # Tu app: aquí la registras para que Django la reconozca
]
  1. Ve a aiven.io → "Get started for free" → regístrate con GitHub o Google.
  2. "Create service" → elige MySQL → plan Free → región cercana a ti → nombra el servicio, ej. bd-impulsatec.
  3. Espera 1-3 minutos a que el estado pase de "Rebuilding" a "Running".
  4. En la pestaña "Overview" copia: Host, Port, User, Password, Database name.
🚫 Importante: la contraseña solo se muestra completa dentro del panel de Aiven. Cópiala a un lugar seguro, no la compartas ni la subas a GitHub.

En la raíz del proyecto crea un archivo llamado exactamente .env (con el punto al inicio):

DB_NAME=defaultdb        # Nombre de la base de datos que te dio Aiven
DB_USER=avnadmin         # Usuario que te dio Aiven
DB_PASSWORD=tu-password  # La contraseña que copiaste del panel
DB_HOST=tu-host.aivencloud.com  # El host que te dio Aiven
DB_PORT=12345             # El puerto que te dio Aiven (no siempre es 3306)
SECRET_KEY=escribe-aqui-una-cadena-larga-y-aleatoria
DEBUG=True                # En local dejamos True para ver errores detallados

Crea también un archivo .gitignore con esta línea, para que .env nunca se suba a GitHub:

.env
# Le dice a Git "ignora este archivo, nunca lo subas"
venv/
# Tampoco subas la carpeta del entorno virtual, es muy pesada e innecesaria en GitHub

Al inicio de settings.py agrega:

import pymysql
# Traemos la librería que traduce entre Django y MySQL
pymysql.install_as_MySQLdb()
# Le dice a Django "usa pymysql como si fuera el conector oficial de MySQL"
from decouple import config
# Traemos la función que lee valores desde el archivo .env

Reemplaza el bloque DATABASES que ya existe por:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',        # Le decimos a Django "vamos a usar MySQL"
        'NAME': config('DB_NAME'),                     # Lee DB_NAME desde el .env
        'USER': config('DB_USER'),                     # Lee DB_USER desde el .env
        'PASSWORD': config('DB_PASSWORD'),             # Lee DB_PASSWORD desde el .env
        'HOST': config('DB_HOST'),                     # Lee DB_HOST desde el .env
        'PORT': config('DB_PORT'),                     # Lee DB_PORT desde el .env
        'OPTIONS': {'ssl': {'ssl-mode': 'REQUIRED'}},  # Aiven exige conexión cifrada (SSL)
    }
}

SECRET_KEY = config('SECRET_KEY')  # La clave secreta también se lee del .env, nunca escrita aquí directo
DEBUG = config('DEBUG', default=False, cast=bool)  # Lee DEBUG del .env y lo convierte a True/False real
python manage.py migrate
# Crea en tu base de datos MySQL las tablas que Django necesita internamente (usuarios, sesiones, etc.)
✅ Debes ver varias líneas terminando en "OK". Si entras al panel de Aiven → pestaña "Current queries" o un cliente MySQL, verás nuevas tablas como auth_user.

Abre catalogo/models.py y escribe:

from django.db import models
# Traemos las herramientas de Django para describir tablas como clases de Python
from django.contrib.auth.models import User
# Traemos el modelo de usuario que Django ya trae incluido

class Servicio(models.Model):
    # Cada clase que hereda de models.Model se convierte en una TABLA en MySQL

    propietario = models.ForeignKey(User, on_delete=models.CASCADE)
    # Relaciona este servicio con el usuario dueño; si se borra el usuario, se borran sus servicios

    titulo = models.CharField(max_length=120)
    # Texto corto (máximo 120 caracteres), obligatorio

    descripcion = models.TextField()
    # Texto largo sin límite fijo de caracteres

    precio = models.DecimalField(max_digits=8, decimal_places=2)
    # Número decimal, hasta 8 dígitos en total y 2 después del punto (ej. 1500.00)

    categoria = models.CharField(max_length=60)
    # Texto corto para clasificar el servicio (ej. "Tutorías", "Diseño")

    creado_en = models.DateTimeField(auto_now_add=True)
    # Guarda automáticamente fecha y hora cuando se crea el registro

    def __str__(self):
        # Define qué texto se muestra cuando Django necesita "imprimir" este objeto (ej. en el admin)
        return self.titulo
        # Mostrará el título en vez de algo como "Servicio object (1)"
python manage.py makemigrations
# Django compara tu modelo con la base de datos y prepara los cambios pendientes (aún no los aplica)

python manage.py migrate
# Ahora sí aplica esos cambios: crea la tabla "catalogo_servicio" en MySQL

Abre catalogo/admin.py:

from django.contrib import admin
# Traemos el panel de administración de Django
from .models import Servicio
# Traemos el modelo que acabamos de crear (el punto significa "de este mismo módulo")

admin.site.register(Servicio)
# Le dice a Django "muestra Servicio dentro del panel /admin para poder gestionarlo visualmente"
python manage.py createsuperuser
# Te pedirá usuario, correo y contraseña para tu cuenta de administrador

python manage.py runserver
# Levanta el servidor local; abre en tu navegador http://127.0.0.1:8000/admin
✅ Debes poder iniciar sesión en /admin y ver "Servicios" en la lista, con un botón para agregar uno nuevo.
pip install gunicorn whitenoise
# gunicorn = servidor que corre Django en Render (runserver es solo para tu computadora)
# whitenoise = permite servir tus archivos CSS/imágenes sin configuración extra

pip freeze > requirements.txt
# Genera una lista con todo lo que instalaste; Render la usa para instalar lo mismo en la nube
git init
# Convierte esta carpeta en un repositorio Git; a partir de aquí Git empieza a "vigilar" los cambios de tus archivos

git add .
# Marca TODOS los archivos como "listos para guardar" (staging); el punto significa "esta carpeta completa"
# Respeta lo que pusiste en .gitignore, así que .env y venv/ NO se agregan

git commit -m "Proyecto Django conectado a MySQL"
# Guarda una "fotografía" fija de todos los archivos marcados, con un mensaje que describe este avance

git branch -M main
# Renombra tu rama de trabajo actual a "main", el nombre estándar que usa GitHub para la rama principal

git remote add origin https://github.com/TU-USUARIO/impulsatec.git
# Registra la dirección de internet donde vivirá la copia de este repositorio; "origin" es el apodo con el que Git la recordará
# Reemplaza TU-USUARIO por tu usuario real de GitHub

git push -u origin main
# Envía tus commits guardados en tu computadora hacia el remoto "origin", a la rama "main"
# -u conecta tu rama local con la remota, para que los próximos "git push" no necesiten repetir "origin main"
  1. render.com → "New +" → "Web Service" → selecciona tu repositorio impulsatec.
  2. Build Command: pip install -r requirements.txt && python manage.py collectstatic --noinput
  3. Start Command: gunicorn impulsatec.wsgi
  4. Instance Type: Free.

En la pestaña "Environment" del servicio, agrega una por una (botón "Add Environment Variable"), copiando los mismos valores de tu .env local:

DB_NAME=defaultdb
DB_USER=avnadmin
DB_PASSWORD=tu-password
DB_HOST=tu-host.aivencloud.com
DB_PORT=12345
SECRET_KEY=la-misma-clave-secreta
DEBUG=False
# En producción SIEMPRE False: oculta detalles técnicos si algo falla
ALLOWED_HOSTS=impulsatec.onrender.com
# Le dice a Django "solo acepta peticiones que lleguen a este dominio"

En settings.py agrega esta línea (si no existe ya):

ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='').split(',')
# Lee la variable de entorno y la separa por comas si hay varios dominios
🚫 Importante: haz clic en "Save Changes" después de cada variable, y "Manual Deploy → Deploy latest commit" al final para que Render las tome en cuenta.

Cuando el deploy termine (círculo verde "Live"), abre la pestaña "Shell" del servicio y escribe:

python manage.py migrate
# Crea las tablas en MySQL, esta vez desde el servidor de Render (misma base de Aiven)

python manage.py createsuperuser
# Crea tu usuario administrador para el sitio ya publicado
✅ Abre https://impulsatec.onrender.com/admin desde cualquier dispositivo: debe pedirte login y, al entrar, mostrar tu tabla "Servicios".
Unidad 3

CRUD Dinámico y Sesiones: que cualquiera lo use sin entrar a /admin

Construyes páginas normales (no el panel de administración) donde un usuario logueado crea, ve, edita y borra sus propios servicios.

Crea el archivo catalogo/forms.py:

from django import forms
# Traemos las herramientas de Django para construir formularios
from .models import Servicio
# Traemos el modelo sobre el que se basará el formulario

class ServicioForm(forms.ModelForm):
    # ModelForm genera automáticamente un formulario a partir de un modelo

    class Meta:
        # Meta es donde le decimos a Django "de qué modelo y qué campos"
        model = Servicio
        # El formulario se basa en el modelo Servicio
        fields = ['titulo', 'descripcion', 'precio', 'categoria']
        # Solo estos 4 campos aparecerán; "propietario" y "creado_en" se llenan automáticos
        widgets = {
            'titulo': forms.TextInput(attrs={'class': 'w-full border rounded-lg p-2'}),
            # Le agregamos clases de Tailwind directo al campo, así se ve bien sin tocar el HTML
            'descripcion': forms.Textarea(attrs={'class': 'w-full border rounded-lg p-2', 'rows': 4}),
        }
from django.shortcuts import render, redirect, get_object_or_404
# render = dibuja una plantilla HTML · redirect = manda al usuario a otra URL
# get_object_or_404 = busca un registro o muestra "página no encontrada" si no existe
from django.contrib.auth.decorators import login_required
# Decorador que exige haber iniciado sesión para entrar a la vista
from .models import Servicio
from .forms import ServicioForm

def lista_servicios(request):
    # Vista pública: cualquiera puede ver el catálogo, sin necesidad de login
    servicios = Servicio.objects.all()
    # Trae TODOS los registros de la tabla Servicio desde MySQL
    return render(request, 'catalogo/lista.html', {'servicios': servicios})
    # Dibuja la plantilla y le pasa la lista de servicios con el nombre "servicios"

@login_required
# Si no has iniciado sesión, Django te redirige automáticamente al login
def crear_servicio(request):
    if request.method == 'POST':
        # POST = el usuario ya llenó y envió el formulario
        form = ServicioForm(request.POST)
        # Cargamos los datos enviados dentro del formulario para validarlos
        if form.is_valid():
            # Revisa que los datos cumplan las reglas del modelo (tipos, longitudes, etc.)
            nuevo = form.save(commit=False)
            # Prepara el objeto pero AÚN no lo guarda en la base de datos (commit=False)
            nuevo.propietario = request.user
            # Asigna como dueño al usuario que inició sesión ahora mismo
            nuevo.save()
            # Ahora sí lo guarda en MySQL
            return redirect('lista_servicios')
            # Redirige al listado para ver el nuevo registro
    else:
        form = ServicioForm()
        # GET = el usuario apenas está entrando a la página; mostramos el formulario vacío
    return render(request, 'catalogo/formulario.html', {'form': form})
@login_required
def editar_servicio(request, id):
    # id llega desde la URL: identifica QUÉ servicio se va a editar
    servicio = get_object_or_404(Servicio, id=id, propietario=request.user)
    # Busca el servicio SOLO si pertenece al usuario actual; si no, error 404 (seguridad)
    if request.method == 'POST':
        form = ServicioForm(request.POST, instance=servicio)
        # instance=servicio le dice al formulario "edita este registro, no crees uno nuevo"
        if form.is_valid():
            form.save()
            return redirect('lista_servicios')
    else:
        form = ServicioForm(instance=servicio)
        # Precarga el formulario con los datos actuales del servicio
    return render(request, 'catalogo/formulario.html', {'form': form})

@login_required
def eliminar_servicio(request, id):
    servicio = get_object_or_404(Servicio, id=id, propietario=request.user)
    if request.method == 'POST':
        servicio.delete()
        # Borra el registro de MySQL de forma permanente
        return redirect('lista_servicios')
    return render(request, 'catalogo/confirmar_eliminar.html', {'servicio': servicio})
    # Muestra una página de "¿seguro que quieres borrar esto?" antes de hacerlo

Crea catalogo/urls.py:

from django.urls import path
from . import views
# Traemos todas las funciones que definimos en views.py

urlpatterns = [
    path('', views.lista_servicios, name='lista_servicios'),
    # La URL vacía "" = la página principal del catálogo
    path('crear/', views.crear_servicio, name='crear_servicio'),
    path('editar/<int:id>/', views.editar_servicio, name='editar_servicio'),
    # <int:id> captura un número de la URL y lo manda como parámetro id a la vista
    path('eliminar/<int:id>/', views.eliminar_servicio, name='eliminar_servicio'),
]

En impulsatec/urls.py (el archivo principal):

from django.contrib import admin
from django.urls import path, include
# include permite "conectar" las urls de otra app

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('catalogo.urls')),
    # Todo lo que empiece en la raíz del sitio se busca dentro de catalogo/urls.py
]

Crea la carpeta catalogo/templates/catalogo/ y adentro lista.html:

<!DOCTYPE html>
<!-- Le dice al navegador "esto es HTML5 moderno" -->

<html lang="es">
<!-- Abre el documento completo; lang="es" ayuda a lectores de pantalla y traductores automáticos -->

<head>
<!-- Configuración que NO se ve dibujada directamente en la página -->

  <meta charset="UTF-8">
  <!-- Permite usar tildes y la ñ sin que se vean rotas -->

  <script src="https://cdn.tailwindcss.com"></script>
  <!-- Descarga Tailwind CSS desde internet; con esto ya puedes usar sus clases de diseño en esta plantilla -->

  <title>Catálogo</title>
  <!-- Texto que aparece en la pestaña del navegador -->

</head>
<body class="bg-slate-50 p-6">
<!-- bg-slate-50 (CSS) = fondo gris muy claro de toda la página -->
<!-- p-6 (CSS) = relleno interno (padding) en los 4 lados del body -->

  <h1 class="text-2xl font-bold mb-4">Catálogo de Servicios</h1>
  <!-- text-2xl (CSS) = letra grande · font-bold (CSS) = negritas · mb-4 (CSS) = margen inferior -->

  <a href="{% url 'crear_servicio' %}" class="inline-block bg-blue-600 text-white px-4 py-2 rounded-lg mb-4">
    <!-- {% url 'crear_servicio' %} genera la URL correcta usando el "name" definido en urls.py -->
    <!-- inline-block (CSS) = se comporta como botón con ancho ajustado a su texto -->
    <!-- bg-blue-600 text-white (CSS) = fondo azul con letras blancas -->
    <!-- px-4 py-2 (CSS) = relleno horizontal/vertical del botón · rounded-lg (CSS) = esquinas redondeadas -->
    + Nuevo servicio
  </a>

  <div class="grid gap-4">
  <!-- grid (CSS) = acomoda a sus hijos en una cuadrícula · gap-4 (CSS) = separación entre tarjetas -->

    {% for servicio in servicios %}
      <!-- {% for %} repite todo lo de adentro una vez por cada servicio de la lista "servicios" que envió views.py -->
      <!-- en cada vuelta, la variable "servicio" representa UN registro distinto de la base de datos -->

      <div class="bg-white p-4 rounded-xl border">
      <!-- bg-white (CSS) = fondo blanco, para que la tarjeta resalte sobre el fondo gris del body -->
      <!-- p-4 (CSS) = relleno interno · rounded-xl (CSS) = esquinas bien redondeadas · border (CSS) = borde delgado gris -->

        <h2 class="font-bold">{{ servicio.titulo }}</h2>
        <!-- font-bold (CSS) = negritas · {{ servicio.titulo }} imprime el título de ESTE servicio -->

        <p class="text-slate-600 text-sm">{{ servicio.descripcion }}</p>
        <!-- text-slate-600 (CSS) = gris medio · text-sm (CSS) = letra pequeña, para texto secundario -->

        <p class="text-blue-700 font-semibold mt-1">${{ servicio.precio }}</p>
        <!-- text-blue-700 (CSS) = azul, resalta el precio · font-semibold (CSS) = seminegrita · mt-1 (CSS) = separación arriba -->

        {% if servicio.propietario == request.user %}
          <!-- {% if %} compara: SOLO entra aquí si TÚ (usuario logueado) eres el dueño de este servicio -->

          <a href="{% url 'editar_servicio' servicio.id %}" class="text-sm text-amber-600">Editar</a>
          <!-- text-amber-600 (CSS) = texto color ámbar para diferenciarlo de un enlace normal -->

          <a href="{% url 'eliminar_servicio' servicio.id %}" class="text-sm text-rose-600 ml-3">Eliminar</a>
          <!-- text-rose-600 (CSS) = texto rojo, comunica una acción destructiva · ml-3 (CSS) = separación a la izquierda del otro enlace -->
        {% endif %}
        <!-- {% endif %} cierra de forma obligatoria el {% if %} abierto arriba -->
      </div>
    {% endfor %}
    <!-- {% endfor %} cierra de forma obligatoria el bucle {% for %} abierto arriba -->
  </div>
</body>
</html>
<!DOCTYPE html>
<!-- Le dice al navegador "esto es HTML5 moderno" -->

<html lang="es">
<!-- Abre el documento; lang="es" ayuda a lectores de pantalla y traductores -->

<head>
  <meta charset="UTF-8">
  <!-- Permite usar tildes y la ñ sin que se vean rotas -->

  <script src="https://cdn.tailwindcss.com"></script>
  <!-- Descarga Tailwind CSS desde internet; habilita sus clases de diseño en esta plantilla -->
</head>
<body class="bg-slate-50 p-6">
<!-- bg-slate-50 (CSS) = fondo gris muy claro · p-6 (CSS) = relleno interno en los 4 lados del body -->

  <form method="POST" class="max-w-md mx-auto bg-white p-6 rounded-xl border space-y-3">
    <!-- method="POST" = los datos se envían de forma segura, no aparecen en la URL -->
    <!-- max-w-md (CSS) = el formulario nunca crece más de ~28rem de ancho · mx-auto (CSS) = lo centra horizontalmente -->
    <!-- bg-white (CSS) = fondo blanco · p-6 (CSS) = relleno interno · rounded-xl (CSS) = esquinas redondeadas -->
    <!-- border (CSS) = borde delgado gris · space-y-3 (CSS) = separación vertical automática entre cada campo hijo -->

    {% csrf_token %}
    <!-- Token de seguridad OBLIGATORIO en todo formulario Django; evita ataques de sitios falsos (CSRF) -->

    {{ form.as_p }}
    <!-- Dibuja automáticamente todos los campos del formulario (definidos en forms.py), cada uno envuelto en un <p> -->

    <button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-lg">Guardar</button>
    <!-- type="submit" = al hacer clic, envía el formulario -->
    <!-- bg-blue-600 text-white (CSS) = botón azul con letras blancas · px-4 py-2 (CSS) = relleno · rounded-lg (CSS) = esquinas redondeadas -->
  </form>
</body>
</html>
🚫 Importante: si olvidas {% csrf_token %}, Django rechaza el formulario con un error 403 "Forbidden".

En impulsatec/urls.py agrega las vistas de autenticación que Django ya trae listas:

from django.contrib.auth import views as auth_views
# Traemos las vistas de login/logout que Django ya trae construidas

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', auth_views.LoginView.as_view(template_name='catalogo/login.html'), name='login'),
    # Usa la vista lista de Django, solo le indicamos qué plantilla mostrar
    path('logout/', auth_views.LogoutView.as_view(next_page='lista_servicios'), name='logout'),
    # next_page = a dónde regresar después de cerrar sesión
    path('', include('catalogo.urls')),
]

Crea catalogo/templates/catalogo/login.html:

<form method="POST" class="max-w-md mx-auto bg-white p-6 rounded-xl border space-y-3 mt-10">
<!-- Misma estructura de tarjeta que formulario.html; mt-10 (CSS) agrega separación arriba para que no pegue con el borde de la pantalla -->

  {% csrf_token %}
  <!-- Token de seguridad obligatorio en todo formulario Django -->

  {{ form.as_p }}
  <!-- Django genera automáticamente los campos de usuario y contraseña (LoginView ya trae ese formulario armado) -->

  <button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-lg">Entrar</button>
  <!-- bg-blue-600 text-white (CSS) = botón azul con letras blancas · px-4 py-2 (CSS) = relleno · rounded-lg (CSS) = esquinas redondeadas -->
</form>

Para el registro (Django no trae una vista lista), crea la vista tú mismo en views.py:

from django.contrib.auth.forms import UserCreationForm
# Formulario de registro que Django ya trae incluido (usuario + contraseña + confirmar)

def registro(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            # Guarda al nuevo usuario, con la contraseña ya cifrada automáticamente
            return redirect('login')
    else:
        form = UserCreationForm()
    return render(request, 'catalogo/registro.html', {'form': form})
  • DEBUG=False en las variables de entorno de Render.
  • SECRET_KEY nunca escrita directo en settings.py, siempre desde variable de entorno.
  • ALLOWED_HOSTS con tu dominio real de Render.
  • Todas las vistas que modifican datos (crear, editar, eliminar) llevan @login_required.
✅ Crea una segunda cuenta de prueba y confirma que NO puede editar ni borrar los servicios de la primera cuenta.
Unidad 4

Proyecto Integrador, Anti-IA y Deployment Final

Cierras, pules y preparas la defensa oral de tu proyecto ya publicado.

En lista.html, agrega un mensaje cuando no haya registros:

{% if not servicios %}
  <!-- "not servicios" es verdadero cuando la lista viene vacía desde views.py -->
  <p class="text-slate-400 text-center py-10">Aún no hay servicios publicados. ¡Sé el primero!</p>
  <!-- text-slate-400 (CSS) = gris tenue, comunica que es un mensaje secundario -->
  <!-- text-center (CSS) = centra el texto horizontalmente · py-10 (CSS) = relleno arriba/abajo generoso, para que "respire" -->
{% endif %}
<!-- {% endif %} cierra de forma obligatoria el {% if %} abierto arriba -->

En GitHub, entra a tu repositorio → pestaña "Commits". Revisa: ¿hay commits repartidos en varias fechas (no solo la última noche)? ¿los mensajes describen qué cambiaste?

⚠️ Cuidado: "arreglos", "cambios", "update" no cuentan como buen mensaje. Usa frases como "Agrego validación de precio mínimo en el formulario".

Desde la Shell de Render, exporta un respaldo de seguridad:

mysqldump -h $DB_HOST -P $DB_PORT -u $DB_USER -p $DB_NAME > respaldo.sql
# Genera un archivo .sql con todo el contenido actual de tu base de datos, por si algo sale mal
git tag -a v1.0 -m "Version final entregada"
# Marca el commit actual como "v1.0", una referencia fija que no cambiará

git push origin v1.0
# Sube esa marca a GitHub para que quede visible en la pestaña "Releases"

Escribe y practica en voz alta la respuesta a 3 preguntas:

  1. ¿Qué hace tu sistema y para quién es útil?
  2. ¿Cómo viajan los datos desde el formulario hasta MySQL?
  3. ¿Qué harías distinto si tuvieras una semana más?
  • La URL de Render abre sin errores 500.
  • Puedes crear, editar y borrar un servicio en vivo, sin usar /admin.
  • Login y logout funcionan con una cuenta de prueba distinta a la tuya.
  • El repositorio tiene un README.md explicando cómo correr el proyecto localmente.
✅ Si marcaste los 4 puntos, estás listo(a) para la defensa oral express de la Unidad 4.

¡Terminaste las 4 Unidades!

Ahora completa tu plantilla de portafolio con los 4 enlaces publicados.

Ir a la plantilla de portafolio