Plantilla de ejecución · Cero conocimientos previos · Versiones 2026
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
| Python | 3.12 o 3.13 (instalador oficial en python.org) |
| Django | 5.1.x o superior (lo instala pip automáticamente en su versión estable más nueva) |
| pymysql | 1.1.x |
| python-decouple | 3.8.x |
| gunicorn / whitenoise | 23.x / 6.8.x |
| MySQL (Aiven) | 8.4 LTS (el que Aiven asigna por defecto) |
| Git / VS Code | Últimas versiones estables (se actualizan solas) |
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.
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?"
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"):
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.
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…
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").
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>© 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.
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
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>
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)
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.
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
(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
]
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.)
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
/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"
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
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
https://impulsatec.onrender.com/admin desde cualquier dispositivo: debe pedirte login y, al entrar, mostrar tu tabla "Servicios".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>
{% 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.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?
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:
¡Terminaste las 4 Unidades!
Ahora completa tu plantilla de portafolio con los 4 enlaces publicados.