Café y Código

6. Django Admin Profesional y Vistas CRUD ⚙️

1. La Metodología Pedagógica: De "Admin Ciego" a Panel SaaS

Uno de los mayores superpoderes de Django frente a otros frameworks backend es supanel de administración automático (django.contrib.admin). Lee los metadatos del ORM y genera al instante una interfaz web completa para crear, buscar, filtrar y auditar registros.

⚠️ El Problema de la Enseñanza Clásica: "El Admin Ciego"

La mayoría de cursos enseñan únicamente: admin.site.register(Vehiculo). El resultado es desastroso para el estudiante: ve una lista gris con nombres comoVehiculo object (1), sin buscador, sin filtros, y sin poder distinguir qué auto está disponible o en reparación. Esto hace que muchos piensen erróneamente que Django Admin es un "juguete obsoleto".

1.1 Creación del Superusuario

Para ingresar al panel de administración necesitas una cuenta con privilegios de superusuario (is_superuser = True). Ejecuta en tu terminal con el entorno virtual (venv) activo:

Creación de Superusuario en CMD
CMD
1(venv) C:\proyectos\django\flota> python manage.py createsuperuser
2Username: admin_flota
3Email address: admin@flotaexpress.cl
4Password: **********
5Password (again): **********
6Superuser created successfully.

1.2 Las 6 Palancas de ModelAdmin: La Escalera de Producción

Para transformar el admin en una herramienta empresarial que cualquier departamento de operaciones pueda usar para gestionar miles de registros, aplicamos las 6 palancas de ModelAdmin:

1. list_display

Reemplaza los objetos crudos por tablas con columnas claras: Patente, Marca, Modelo y datos formateados.

2. format_html (Badges)

Convierte estados de texto en etiquetas visuales con colores (Verde = Disponible, Rojo = En Taller).

3. search_fields

Habilita búsqueda instantánea en campos directos y relaciones con claves foráneas (piloto__nombre).

4. list_filter

Crea un panel lateral facetado con conteos automáticos por estado, tipo de vehículo o rangos de fecha.

5. list_editable

Convierte celdas en inputs interactivos (estilo Excel/Google Sheets) para editar stock o estado sin entrar a cada ficha.

6. TabularInline & Actions

Patrón Maestro-Detalle (editar vehículo y sus revisiones técnicas en 1 sola pantalla) y acciones masivas en lote.

🧪 Laboratorio Interactivo: Prueba las 6 Palancas y el Rediseño con Unfold

Interactúa con los controles inferiores: activa cada nivel para observar cómo evoluciona la interfaz y pulsa el botón superior para alternar entre el diseño Django Clásico (2005) yDjango Unfold (Tailwind CSS 2026):

Laboratorio InteractivoPaso a Paso Didáctico

⚙️ Django Admin Studio:De "Admin Ciego" a Panel SaaS 2026

🎯 Activa los Superpoderes de ModelAdmin en Vivo:(Haz clic en cada paso para ver la transformación)
FE
Flota Express ControlUnfold v0.42
Panel Operativo Inteligente (Tailwind)
Avatar
Inicio›Flota›Vehículos
Total Flota
6100% inventario
Disponibles
2Para despacho
En Ruta
2En tránsito
En Taller
2Revisión req.
⚠️ search_fields deshabilitado (Nivel 0 a 2: el admin no tiene buscador).
Activa Paso 7 para habilitar @admin.action
PatenteVehículoPiloto AsignadoEstadoKilometraje
FL-204-CL
Toyota Hilux 4x4 D-Cab
CAMIONETA • 2023
Carlos MendozaCarlos Mendoza
DISPONIBLE42,500 km
VK-881-TX
Volvo FH16 750 Tracto
CAMION • 2022
Valentina RojasValentina Rojas
EN_RUTA118,000 km
MB-550-SP
Mercedes-Benz Sprinter 516 Maxi
VAN • 2024
Matías SilvaMatías Silva
MANTENIMIENTO18,400 km
HY-309-AC
Hyundai Accent Prime 1.6
AUTO • 2021
Camila SotoCamila Soto
DISPONIBLE68,900 km
SC-910-XT
Scania R500 Super V8
CAMION • 2023
Javier MoralesJavier Morales
EN_RUTA89,300 km
CH-774-DM
Chevrolet D-Max 3.0 4x4
CAMIONETA • 2022
Daniela CastilloDaniela Castillo
MANTENIMIENTO54,100 km
Mostrando 6 de 6 vehículos registrados.
# flota/admin.py (Django Unfold - Nivel 2: ModelAdmin Avanzado)
from django.contrib import admin
from django.utils.html import format_html
from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import action, display
from .models import Vehiculo, Mantenimiento, Piloto



@admin.register(Vehiculo)
class VehiculoAdmin(ModelAdmin):
    # 1. Configuración de Unfold
    warn_unsaved_form = True
    list_filter_submit = True
    list_per_page = 15

    # 2. Columnas en la tabla principal
    list_display = [
        "patente",
        "marca_modelo",
        "piloto_card",
        "estado_badge",
        "kilometraje_fmt",
    ]





    @display(description="Vehículo")
    def marca_modelo(self, obj):
        return f"{obj.marca} {obj.modelo} ({obj.anio})"

    @display(description="Piloto Asignado")
    def piloto_card(self, obj):
        if not obj.piloto:
            return "Sin Asignar"
        return format_html(
            '<div class="flex items-center gap-2">'
            '<img src="{}" class="w-6 h-6 rounded-full"/>'
            '<span>{}</span></div>',
            obj.piloto.avatar.url, obj.piloto.nombre
        )

    @display(description="Estado Operativo", label=True)
    def estado_badge(self, obj):
        # Unfold renderiza automáticamente estilos modernos con labels
        colores = {
            "DISPONIBLE": "emerald",
            "EN_RUTA": "blue",
            "MANTENIMIENTO": "rose",
        }
        color = colores.get(obj.estado, "gray")
        return obj.estado, color

1.3 Rediseño Visual Radical: De Django 2005 a Django Unfold (Tailwind CSS 2026)

Aunque ModelAdmin otorga funcionalidad completa, la estética por defecto de Django (diseñada a inicios de los 2000) luce anticuada para entregar a clientes o gerencias. En el ecosistema Python moderno existe un proyecto de código abierto que se ha convertido en el estándar indiscutible:django-unfold.

🚀 django-unfold (Recomendado)Tailwind CSS

El tema más moderno del ecosistema: Soporte nativo de Modo Oscuro/Claro, tarjetas KPI de métricas en el Dashboard, gráficos con Chart.js, responsive real para celulares, y componentes visuales ricos (badges, progress bars, tabs).

pip install django-unfold
🏛️ django-jazzmin (Alternativa Clásica)Bootstrap 4

Tema clásico basado en Bootstrap 4 y AdminLTE. Muy popular durante la última década con menú horizontal o lateral tradicional. Ideal si el resto de tu sitio ya utiliza Bootstrap.

pip install django-jazzmin

Paso 1: Instalación y la "Regla de Oro" de settings.py

Para que Django cargue las plantillas modernas de Unfold antes que las plantillas por defecto del framework,"unfold" debe colocarse ANTES de "django.contrib.admin" en la lista INSTALLED_APPS:

config/settings.py (Configuración de Unfold)
PYTHON
1# config/settings.py
2
3INSTALLED_APPS = [
4 # ⚠️ REGLA DE ORO: 'unfold' debe ir ANTES de 'django.contrib.admin'
5 "unfold",
6 "unfold.contrib.filters", # Filtros facetados con estilo Tailwind
7 "unfold.contrib.forms", # Widgets modernos y selects estilizados
8 "unfold.contrib.inlines", # Inlines colapsables y ordenables
9
10 # Apps del núcleo de Django
11 "django.contrib.admin",
12 "django.contrib.auth",
13 "django.contrib.contenttypes",
14 "django.contrib.sessions",
15 "django.contrib.messages",
16 "django.contrib.staticfiles",
17
18 # Tus aplicaciones
19 "flota.apps.FlotaConfig",
20]
21
22# Configuración Visual de Marca con Unfold
23UNFOLD = {
24 "SITE_TITLE": "Flota Express Control",
25 "SITE_HEADER": "Flota Express SaaS",
26 "SITE_SUBHEADER": "Panel de Operaciones de Flota",
27 "SITE_URL": "/admin/",
28 "THEME": "dark", # 'light', 'dark', o 'auto' según preferencia del sistema
29 "COLORS": {
30 "primary": {
31 "500": "20 184 166", # Teal moderno (#14b8a6)
32 "600": "13 148 136",
33 "700": "15 118 110",
34 },
35 },
36 "SIDEBAR": {
37 "show_search": True,
38 "show_all_applications": True,
39 },
40}

Paso 2: Escribir admin.py con Unfold y Componentes Ricos

En tus archivos admin.py, en vez de heredar de admin.ModelAdmin, heredas deunfold.admin.ModelAdmin. Esto desbloquea decoradores visuales como @display(label=True)para badges automáticos y acciones estilizadas:

flota/admin.py (ModelAdmin Profesional con Django Unfold)
PYTHON
1# flota/admin.py
2from django.contrib import admin
3from django.utils.html import format_html
4from unfold.admin import ModelAdmin, TabularInline
5from unfold.decorators import action, display
6from .models import Vehiculo, Mantenimiento, Piloto
7
8
9class MantenimientoInline(TabularInline):
10 """Permite registrar revisiones mecánicas directamente dentro del Vehículo."""
11 model = Mantenimiento
12 extra = 0
13 fields = ["fecha", "tipo_servicio", "costo", "completado"]
14 readonly_fields = ["fecha"]
15
16
17@action(description="🛠️ Enviar seleccionados a Mantenimiento", permissions=["change"])
18def enviar_a_taller(modeladmin, request, queryset):
19 afectados = queryset.update(estado="MANTENIMIENTO")
20 modeladmin.message_user(request, f"{afectados} vehículo(s) han sido derivados al taller técnico.")
21
22
23@action(description="✅ Liberar seleccionados como Disponibles", permissions=["change"])
24def liberar_disponible(modeladmin, request, queryset):
25 afectados = queryset.update(estado="DISPONIBLE")
26 modeladmin.message_user(request, f"{afectados} vehículo(s) marcados como listos para salida.")
27
28
29@admin.register(Vehiculo)
30class VehiculoAdmin(ModelAdmin):
31 # Opciones exclusivas de Unfold para productividad UX
32 warn_unsaved_form = True # Alerta si sales de la página sin guardar cambios
33 list_filter_submit = True # Añade botón "Filtrar" para evitar recargas constantes
34 list_per_page = 20
35
36 # 1. Columnas organizadas
37 list_display = ["patente", "vehiculo_info", "piloto_badge", "estado_badge", "kilometraje_fmt"]
38
39 # 2. Edición rápida directa en tabla (Excel-like)
40 list_editable = ["estado"]
41
42 # 3. Búsqueda directa y a través de relaciones
43 search_fields = ["patente", "marca", "modelo", "piloto__nombre", "piloto__rut"]
44
45 # 4. Filtros laterales facetados
46 list_filter = ["estado", "tipo", "anio"]
47
48 # 5. Inlines (Maestro-Detalle) y Acciones Masivas
49 inlines = [MantenimientoInline]
50 actions = [enviar_a_taller, liberar_disponible]
51
52 @display(description="Vehículo")
53 def vehiculo_info(self, obj):
54 return f"{obj.marca} {obj.modelo} ({obj.anio})"
55
56 @display(description="Piloto Responsable")
57 def piloto_badge(self, obj):
58 if not obj.piloto:
59 return format_html('<span style="color: #94a3b8; font-style: italic;">Sin asignar</span>')
60 return obj.piloto.nombre
61
62 @display(description="Estado Operativo", label=True)
63 def estado_badge(self, obj):
64 """label=True renderiza automáticamente badges con colores vivos de Tailwind"""
65 colores = {
66 "DISPONIBLE": "emerald",
67 "EN_RUTA": "blue",
68 "MANTENIMIENTO": "rose",
69 }
70 return obj.estado, colores.get(obj.estado, "gray")
71
72 @display(description="Kilometraje", ordering="kilometraje")
73 def kilometraje_fmt(self, obj):
74 return f"{obj.kilometraje:,} km".replace(",", ".")

2. Arquitectura de Cada Vista CRUD (Create, Read, Update, Delete)

El acrónimo CRUD define las cuatro operaciones fundamentales sobre cualquier base de datos:

C — Create (Crear):Presenta un formulario vacío mediante GET y procesa la carga de datos en base de datos mediante POST con validaciones de tipo e integridad.
R — Read (Leer / Listar):Consulta registros en base de datos para presentarlos en catálogos paginados (ListView) o en fichas individuales (DetailView).
U — Update (Actualizar / Editar):Carga la instancia existente en el formulario (instance=obj), permite modificar sus atributos y aplica validaciones antes de guardar.
D — Delete (Eliminar):Solicita confirmación mediante POST para prevenir borrados accidentales y ejecuta obj.delete() con chequeo de integridad referencial.

2.1 Formularios Basados en Modelos (ModelForm)

En Django, no escribes formularios HTML a mano. Utilizas forms.ModelForm, que genera los campos, etiquetas y validaciones directamente a partir de la definición de tu modelo:

biblioteca/forms.py
PYTHON
1# biblioteca/forms.py
2from django import forms
3from .models import Libro
4
5
6class LibroForm(forms.ModelForm):
7 class Meta:
8 model = Libro
9 fields = [
10 'isbn', 'titulo', 'autor', 'categoria', 'tags',
11 'resumen', 'precio_reposicion', 'stock_disponible',
12 'disponible_para_prestamo', 'idioma', 'fecha_publicacion'
13 ]
14 widgets = {
15 'fecha_publicacion': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
16 'resumen': forms.Textarea(attrs={'rows': 4, 'placeholder': 'Sinopsis o resumen del libro...'}),
17 'precio_reposicion': forms.NumberInput(attrs={'step': '0.01', 'min': '0.01'}),
18 }
19
20 # Validación de Campo Único (clean_<nombre_campo>)
21 def clean_isbn(self):
22 isbn = self.cleaned_data.get('isbn', '').strip().replace('-', '')
23 if len(isbn) not in [10, 13]:
24 raise forms.ValidationError("El ISBN debe contener exactamente 10 o 13 dígitos numéricos.")
25 return isbn
26
27 # Validación Cruzada Global (clean)
28 def clean(self):
29 cleaned_data = super().clean()
30 stock = cleaned_data.get('stock_disponible')
31 disponible = cleaned_data.get('disponible_para_prestamo')
32
33 if disponible and (stock is None or stock <= 0):
34 raise forms.ValidationError({
35 'stock_disponible': "No puedes marcar el libro como disponible si el stock es cero."
36 })
37 return cleaned_data

2.2 Implementación de Cada Vista CRUD (FBV vs CBV)

A continuación comparamos cómo se implementan las 4 operaciones mediante funciones (FBV) y mediante clases genéricas (CBV):

biblioteca/views_crud.py
PYTHON
1# biblioteca/views_crud.py
2from django.shortcuts import render, get_object_or_404, redirect
3from django.urls import reverse_lazy
4from django.contrib import messages
5from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
6from .models import Libro
7from .forms import LibroForm
8
9
10# ===============================================================
11# 1. CREATE (Crear Nuevo Libro)
12# ===============================================================
13# A. Como FBV:
14def libro_crear_fbv(request):
15 if request.method == 'POST':
16 form = LibroForm(request.POST, request.FILES)
17 if form.is_valid():
18 nuevo_libro = form.save()
19 messages.success(request, f"¡Libro '{nuevo_libro.titulo}' registrado con éxito!")
20 return redirect('biblioteca:libro_detalle_id', pk=nuevo_libro.id)
21 else:
22 form = LibroForm()
23 return render(request, 'biblioteca/libro_form.html', {'form': form, 'accion': 'Crear'})
24
25# B. Como CBV:
26class LibroCreateView(CreateView):
27 model = Libro
28 form_class = LibroForm
29 template_name = 'biblioteca/libro_form.html'
30 success_url = reverse_lazy('biblioteca:catalogo')
31
32 def form_valid(self, form):
33 messages.success(self.request, "Libro creado exitosamente.")
34 return super().form_valid(form)
35
36
37# ===============================================================
38# 2. UPDATE (Editar Libro Existente)
39# ===============================================================
40# A. Como FBV (Importante: pasar instance=libro):
41def libro_editar_fbv(request, pk):
42 libro = get_object_or_404(Libro, pk=pk)
43 if request.method == 'POST':
44 # Al pasar instance=libro, form.save() actualiza el registro en vez de crear uno nuevo
45 form = LibroForm(request.POST, request.FILES, instance=libro)
46 if form.is_valid():
47 form.save()
48 messages.info(request, f"Libro '{libro.titulo}' actualizado.")
49 return redirect('biblioteca:libro_detalle_id', pk=libro.id)
50 else:
51 form = LibroForm(instance=libro)
52 return render(request, 'biblioteca/libro_form.html', {'form': form, 'libro': libro, 'accion': 'Editar'})
53
54# B. Como CBV:
55class LibroUpdateView(UpdateView):
56 model = Libro
57 form_class = LibroForm
58 template_name = 'biblioteca/libro_form.html'
59 success_url = reverse_lazy('biblioteca:catalogo')
60
61
62# ===============================================================
63# 3. DELETE (Eliminar Libro)
64# ===============================================================
65# A. Como FBV:
66def libro_eliminar_fbv(request, pk):
67 libro = get_object_or_404(Libro, pk=pk)
68 if request.method == 'POST': # Siempre validar que la eliminación sea por POST
69 titulo = libro.titulo
70 libro.delete()
71 messages.warning(request, f"El libro '{titulo}' ha sido eliminado.")
72 return redirect('biblioteca:catalogo')
73 return render(request, 'biblioteca/libro_confirm_delete.html', {'libro': libro})
74
75# B. Como CBV:
76class LibroDeleteView(DeleteView):
77 model = Libro
78 template_name = 'biblioteca/libro_confirm_delete.html'
79 success_url = reverse_lazy('biblioteca:catalogo')

⚡ Laboratorio Interactivo: Simulador de Django Admin y CRUD

Experimenta el ciclo de vida completo de un CRUD en tiempo real: crea registros pasando por las validaciones de clean(), busca y filtra como en Django Admin, edita instancias existentes y elimina elementos con confirmación.

⚙️

Django Admin & CRUD Simulator

Experimenta el panel administrativo, formularios y ciclo Create-Read-Update-Delete

🔍
Filtro:
IDTítulo del LibroCategoríaPrecio VentaCostoStockEstadoAcciones CRUD
#1Clean Code: Manual de desarrollo ágilIngeniería$45.00$28.004 u.● Activo
#2Python Fluente 2da EdiciónBackend$65.00$42.002 u.● Activo
#3Django 5 ProfesionalBackend$55.00$34.006 u.● Activo
#4Patrones de Diseño (GoF)Arquitectura$50.00$30.000 u.○ Inactivo

3. Ejercicios Didácticos: Proyecto 1 (Biblioteca)

Aplica las técnicas de administración y formularios sobre el dominio de la biblioteca.

Ejercicio 3.1: Formulario con Validación de Préstamos

Crea un ModelForm para SolicitudPrestamo que impida fijar una fecha de devolución superior a 30 días en el futuro.

Ver Solución Explicada
solucion_prestamo_form.py
PYTHON
1from django import forms
2from django.utils import timezone
3from datetime import timedelta
4from .models import SolicitudPrestamo
5
6class SolicitudPrestamoForm(forms.ModelForm):
7 class Meta:
8 model = SolicitudPrestamo
9 fields = ['libro', 'fecha_devolucion_limite', 'observaciones']
10
11 def clean_fecha_devolucion_limite(self):
12 fecha_limite = self.cleaned_data.get('fecha_devolucion_limite')
13 hoy = timezone.now().date()
14 maximo_permitido = hoy + timedelta(days=30)
15
16 if fecha_limite < hoy:
17 raise forms.ValidationError("La fecha de devolución no puede ser una fecha pasada.")
18 if fecha_limite > maximo_permitido:
19 raise forms.ValidationError("El período máximo de préstamo de la biblioteca es de 30 días.")
20 return fecha_limite

Ejercicio 3.2: Acción Personalizada en Django Admin con Mensajes

Escribe una acción en SolicitudPrestamoAdmin que permita seleccionar múltiples solicitudes atrasadas y condonar sus multas acumuladas poniéndolas en $0.00.

Ver Solución Explicada
solucion_condonar_admin.py
PYTHON
1@admin.action(description="Condonar multas acumuladas a los préstamos seleccionados")
2def condonar_multas(modeladmin, request, queryset):
3 afectados = queryset.filter(multa_acumulada__gt=0).update(multa_acumulada=0.00)
4 modeladmin.message_user(
5 request,
6 f"Se han condonado las multas de {afectados} préstamo(s) con éxito.",
7 messages.SUCCESS
8 )
9
10@admin.register(SolicitudPrestamo)
11class SolicitudPrestamoAdmin(admin.ModelAdmin):
12 list_display = ['id', 'usuario', 'libro', 'estado', 'multa_acumulada']
13 actions = [condonar_multas]

4. Desafío Práctico: Segundo Proyecto — Tienda Online (`tienda`) 🛒

Implementa el panel administrativo y los formularios CRUD del segundo proyecto ubicado en C:\proyectos\django\tienda.

Reto 4.1: PedidoAdmin con TabularInline de Ítems y Acción de Despacho

En pedidos/admin.py, registra Pedido con una relación tabular de ItemPedidoInline para ver los productos comprados, cantidades y subtotales en la misma vista de administración. Añade una acción para marcar los pedidos seleccionados como 'ENVIADO'.

Ver Solución Explicada
solucion_pedido_admin.py
PYTHON
1# pedidos/admin.py
2from django.contrib import admin
3from .models import Pedido, ItemPedido
4
5class ItemPedidoInline(admin.TabularInline):
6 model = ItemPedido
7 extra = 0
8 raw_id_fields = ['producto']
9 readonly_fields = ['precio_unitario', 'subtotal']
10
11@admin.action(description="Marcar pedidos seleccionados como ENVIADOS")
12def marcar_como_enviado(modeladmin, request, queryset):
13 actualizados = queryset.filter(estado='PAGADO').update(estado='ENVIADO')
14 modeladmin.message_user(request, f"{actualizados} pedidos marcados como despachados.")
15
16@admin.register(Pedido)
17class PedidoAdmin(admin.ModelAdmin):
18 list_display = ['id', 'numero_seguimiento', 'cliente', 'estado', 'total', 'fecha_creacion']
19 list_filter = ['estado', 'fecha_creacion']
20 search_fields = ['id', 'cliente__user__username', 'numero_seguimiento']
21 inlines = [ItemPedidoInline]
22 actions = [marcar_como_enviado]

Reto 4.2: ModelForm con Validación Cruzada (Precio vs Costo)

En catalogo/forms.py, crea ProductoForm garantizando que: 1. El SKU se guarde siempre en mayúsculas y tenga al menos 4 caracteres. 2. El precio de venta nunca sea inferior al costo de adquisición.

Ver Solución Explicada
solucion_producto_form.py
PYTHON
1# catalogo/forms.py
2from django import forms
3from .models import Producto
4
5class ProductoForm(forms.ModelForm):
6 class Meta:
7 model = Producto
8 fields = ['sku', 'nombre', 'categoria', 'descripcion', 'precio', 'costo', 'stock', 'destacado']
9
10 def clean_sku(self):
11 sku = self.cleaned_data.get('sku', '').strip().upper()
12 if len(sku) < 4:
13 raise forms.ValidationError("El código SKU debe contener al menos 4 caracteres.")
14 return sku
15
16 def clean(self):
17 cleaned_data = super().clean()
18 precio = cleaned_data.get('precio')
19 costo = cleaned_data.get('costo')
20
21 if precio is not None and costo is not None:
22 if precio < costo:
23 raise forms.ValidationError({
24 'precio': "El precio de venta no puede ser menor al costo comercial del producto."
25 })
26 return cleaned_data

Reto 4.3: Vista de Eliminación Suave (Soft Delete)

En comercio electrónico nunca se borra físicamente un producto si ya forma parte del historial de compras de clientes pasados (provocaría error de integridad referencial). Escribe una vista FBV desactivar_producto(request, pk) que al recibir un POST cambie disponible = False en vez de ejecutar delete().

Ver Solución Explicada
soft_delete_view.py
PYTHON
1# catalogo/views.py
2from django.shortcuts import get_object_or_404, redirect, render
3from django.contrib import messages
4from .models import Producto
5
6def desactivar_producto_view(request, pk):
7 producto = get_object_or_404(Producto, pk=pk)
8
9 if request.method == 'POST':
10 # Soft delete: se desactiva sin borrarlo de la base de datos
11 producto.disponible = False
12 producto.save(update_fields=['disponible'])
13 messages.warning(request, f"El producto '{producto.nombre}' ha sido desactivado del catálogo.")
14 return redirect('catalogo:lista')
15
16 return render(request, 'catalogo/producto_confirm_deactivate.html', {'producto': producto})

Ponte a prueba

Comprueba que dominas el panel de administración, la arquitectura CRUD y la validación de formularios en Django.

Ko-fi
Donaciones
Apoyá cafeycodigo con un café en Ko-fi. Colaboradores: insignia, muro y zona exclusiva.