Proyecto CRUD Completo: Sistema de Gestión de Biblioteca

Objetivo de aprendizaje: En este módulo final, construiremos una aplicación web completa de gestión de biblioteca que integra todos los conceptos aprendidos en los módulos anteriores.

Este proyecto práctico nos permitirá aplicar todo lo que hemos aprendido sobre Django, desde la configuración inicial hasta el despliegue, creando una aplicación web funcional y profesional.

Características del Proyecto
  • Gestión de libros y autores
  • Sistema de préstamos
  • Gestión de usuarios
  • Panel de administración
  • Búsqueda y filtros
  • Sistema de reservas
  • Notificaciones
  • Reportes y estadísticas
Este proyecto es un ejemplo real de cómo las diferentes partes de Django trabajan juntas para crear una aplicación web completa y funcional.

Planificación del Proyecto

Antes de empezar a codificar, es importante planificar la estructura y funcionalidades de nuestra aplicación.

Requerimientos Funcionales
Módulo Funcionalidades
Gestión de Libros
  • CRUD de libros
  • Gestión de categorías
  • Control de inventario
Gestión de Usuarios
  • Registro y autenticación
  • Perfiles de usuario
  • Roles y permisos
Sistema de Préstamos
  • Préstamo de libros
  • Devoluciones
  • Historial de préstamos

Estructura del Proyecto

Organizaremos nuestro proyecto en múltiples aplicaciones Django para mantener el código modular y mantenible.

Estructura de Directorios
biblioteca/
    ├── manage.py
    ├── biblioteca/
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── libros/
    │   ├── models.py
    │   ├── views.py
    │   ├── forms.py
    │   └── urls.py
    ├── usuarios/
    │   ├── models.py
    │   ├── views.py
    │   └── forms.py
    ├── prestamos/
    │   ├── models.py
    │   ├── views.py
    │   └── forms.py
    └── templates/
        ├── base.html
        ├── libros/
        ├── usuarios/
        └── prestamos/
Importante: Una buena estructura de proyecto facilita:
  • Mantenimiento del código
  • Colaboración en equipo
  • Escalabilidad de la aplicación

Modelos y Base de Datos

Implementaremos los modelos necesarios para nuestra aplicación de biblioteca.

Modelo de Libro
# libros/models.py
from django.db import models
from django.urls import reverse

class Categoria(models.Model):
    nombre = models.CharField(max_length=100)
    descripcion = models.TextField(blank=True)
    
    class Meta:
        verbose_name_plural = "Categorías"
    
    def __str__(self):
        return self.nombre

class Autor(models.Model):
    nombre = models.CharField(max_length=200)
    biografia = models.TextField(blank=True)
    fecha_nacimiento = models.DateField(null=True, blank=True)
    
    def __str__(self):
        return self.nombre

class Libro(models.Model):
    titulo = models.CharField(max_length=200)
    isbn = models.CharField('ISBN', max_length=13, unique=True)
    autor = models.ForeignKey(Autor, on_delete=models.PROTECT, related_name='libros')
    categorias = models.ManyToManyField(Categoria, related_name='libros')
    descripcion = models.TextField()
    portada = models.ImageField(upload_to='portadas/', null=True, blank=True)
    copias_disponibles = models.PositiveIntegerField(default=1)
    fecha_adicion = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.titulo
    
    def get_absolute_url(self):
        return reverse('libro_detalle', args=[str(self.id)])
Modelo de Préstamo
# prestamos/models.py
from django.db import models
from django.contrib.auth.models import User
from libros.models import Libro
from django.utils import timezone
from datetime import timedelta

class Prestamo(models.Model):
    ESTADO_CHOICES = [
        ('prestado', 'Prestado'),
        ('devuelto', 'Devuelto'),
        ('vencido', 'Vencido'),
    ]
    
    libro = models.ForeignKey(Libro, on_delete=models.PROTECT)
    usuario = models.ForeignKey(User, on_delete=models.CASCADE)
    fecha_prestamo = models.DateTimeField(auto_now_add=True)
    fecha_devolucion_esperada = models.DateTimeField()
    fecha_devolucion_real = models.DateTimeField(null=True, blank=True)
    estado = models.CharField(max_length=10, choices=ESTADO_CHOICES, default='prestado')
    
    def save(self, *args, **kwargs):
        if not self.fecha_devolucion_esperada:
            # Por defecto, 14 días de préstamo
            self.fecha_devolucion_esperada = timezone.now() + timedelta(days=14)
        super().save(*args, **kwargs)
    
    def esta_vencido(self):
        return self.estado == 'prestado' and timezone.now() > self.fecha_devolucion_esperada
    
    def __str__(self):
        return f"{self.libro.titulo} - {self.usuario.username}"
Los modelos incluyen validaciones y métodos útiles que facilitarán el desarrollo de las vistas y la lógica de negocio.

Vistas y Formularios

Implementaremos las vistas necesarias para manejar las operaciones CRUD y los formularios para la entrada de datos.

Formularios
# libros/forms.py
from django import forms
from .models import Libro, Autor, Categoria

class LibroForm(forms.ModelForm):
    class Meta:
        model = Libro
        fields = ['titulo', 'isbn', 'autor', 'categorias', 'descripcion', 'portada', 'copias_disponibles']
        widgets = {
            'descripcion': forms.Textarea(attrs={'rows': 4}),
            'categorias': forms.CheckboxSelectMultiple(),
        }

class PrestamoForm(forms.ModelForm):
    class Meta:
        model = Prestamo
        fields = ['libro']
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['libro'].queryset = Libro.objects.filter(copias_disponibles__gt=0)
Vistas Basadas en Clase
# libros/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.urls import reverse_lazy
from .models import Libro
from .forms import LibroForm

class LibroListView(ListView):
    model = Libro
    context_object_name = 'libros'
    template_name = 'libros/libro_list.html'
    paginate_by = 10
    
    def get_queryset(self):
        queryset = Libro.objects.all()
        busqueda = self.request.GET.get('buscar')
        if busqueda:
            queryset = queryset.filter(titulo__icontains=busqueda)
        return queryset

class LibroCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
    model = Libro
    form_class = LibroForm
    template_name = 'libros/libro_form.html'
    success_url = reverse_lazy('libro_list')
    permission_required = 'libros.add_libro'

class LibroUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
    model = Libro
    form_class = LibroForm
    template_name = 'libros/libro_form.html'
    permission_required = 'libros.change_libro'

class LibroDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
    model = Libro
    success_url = reverse_lazy('libro_list')
    template_name = 'libros/libro_confirm_delete.html'
    permission_required = 'libros.delete_libro'
Vistas de Préstamos
# prestamos/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Prestamo
from .forms import PrestamoForm

@login_required
def solicitar_prestamo(request):
    if request.method == 'POST':
        form = PrestamoForm(request.POST)
        if form.is_valid():
            prestamo = form.save(commit=False)
            prestamo.usuario = request.user
            libro = form.cleaned_data['libro']
            if libro.copias_disponibles > 0:
                libro.copias_disponibles -= 1
                libro.save()
                prestamo.save()
                messages.success(request, 'Préstamo realizado con éxito!')
                return redirect('mis_prestamos')
            else:
                messages.error(request, 'No hay copias disponibles de este libro.')
    else:
        form = PrestamoForm()
    return render(request, 'prestamos/solicitar_prestamo.html', {'form': form})

@login_required
def devolver_libro(request, prestamo_id):
    prestamo = get_object_or_404(Prestamo, id=prestamo_id, usuario=request.user)
    if prestamo.estado == 'prestado':
        prestamo.estado = 'devuelto'
        prestamo.fecha_devolucion_real = timezone.now()
        prestamo.save()
        prestamo.libro.copias_disponibles += 1
        prestamo.libro.save()
        messages.success(request, 'Libro devuelto con éxito!')
    return redirect('mis_prestamos')
Importante: Las vistas incluyen:
  • Validación de permisos
  • Manejo de transacciones
  • Mensajes de retroalimentación
  • Redirecciones apropiadas

Templates y Frontend

Crearemos templates reutilizables y un frontend moderno usando Bootstrap.

Template Base
<!-- templates/base.html -->
{% raw %}
{% load static %}



    
    
    {% block title %}Biblioteca{% endblock %}
    
    


    

    
{% if messages %} {% for message in messages %}
{{ message }}
{% endfor %} {% endif %} {% block content %}{% endblock %}

© 2025 Biblioteca. Todos los derechos reservados.

{% endraw %}
Lista de Libros
<!-- templates/libros/libro_list.html -->
{% raw %}
{% extends 'base.html' %}

{% block title %}Libros | {{ block.super }}{% endblock %}

{% block content %}

Catálogo de Libros

{% for libro in libros %}
{% if libro.portada %} {{ libro.titulo }} {% endif %}
{{ libro.titulo }}

Por {{ libro.autor.nombre }}

{{ libro.descripcion|truncatewords:30 }}

{% empty %}
No se encontraron libros.
{% endfor %}
{% endblock %} {% endraw %}
Consejos de diseño:
  • Utiliza los tags {% raw %}{% endraw %} para mostrar correctamente la sintaxis de Django en los ejemplos
  • Mantén un diseño consistente usando Bootstrap en todos los templates
  • Implementa componentes reutilizables para elementos comunes
  • Agrega comentarios explicativos en las secciones complejas

Autenticación y Permisos

Implementaremos un sistema de autenticación y autorización robusto para controlar el acceso a las diferentes funcionalidades.

Grupos de Usuarios
# usuarios/permissions.py
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from libros.models import Libro
from prestamos.models import Prestamo

def crear_grupos_permisos():
    # Crear grupo de Bibliotecarios
    bibliotecarios, _ = Group.objects.get_or_create(name='Bibliotecarios')
    
    # Obtener todos los permisos para Libros y Préstamos
    libro_ct = ContentType.objects.get_for_model(Libro)
    prestamo_ct = ContentType.objects.get_for_model(Prestamo)
    
    permisos_libro = Permission.objects.filter(content_type=libro_ct)
    permisos_prestamo = Permission.objects.filter(content_type=prestamo_ct)
    
    # Asignar permisos al grupo
    bibliotecarios.permissions.set([*permisos_libro, *permisos_prestamo])
    
    # Crear grupo de Usuarios Regulares
    usuarios, _ = Group.objects.get_or_create(name='Usuarios')
    
    # Asignar permisos limitados
    ver_libro = Permission.objects.get(codename='view_libro', content_type=libro_ct)
    ver_prestamo = Permission.objects.get(codename='view_prestamo', content_type=prestamo_ct)
    add_prestamo = Permission.objects.get(codename='add_prestamo', content_type=prestamo_ct)
    
    usuarios.permissions.set([ver_libro, ver_prestamo, add_prestamo])
Decoradores y Mixins
# usuarios/decorators.py
from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import PermissionDenied
from functools import wraps

def es_bibliotecario(user):
    return user.groups.filter(name='Bibliotecarios').exists()

def bibliotecario_required(view_func):
    @wraps(view_func)
    def _wrapped_view(request, *args, **kwargs):
        if not es_bibliotecario(request.user):
            raise PermissionDenied
        return view_func(request, *args, **kwargs)
    return _wrapped_view

# Uso en vistas
@login_required
@bibliotecario_required
def gestionar_prestamos(request):
    # Solo bibliotecarios pueden acceder
    prestamos = Prestamo.objects.all()
    return render(request, 'prestamos/gestionar.html', {'prestamos': prestamos})
Seguridad:
  • Implementa autenticación en todas las vistas sensibles
  • Usa decoradores y mixins para control de acceso
  • Verifica permisos tanto en backend como en frontend
  • Mantén logs de acciones importantes

Despliegue

Preparar y desplegar una aplicación Django en producción requiere varias consideraciones importantes.

Configuración de Producción
# biblioteca/settings/production.py
from .base import *

DEBUG = False
ALLOWED_HOSTS = ['biblioteca.example.com', 'www.biblioteca.example.com']

# Configuración de base de datos PostgreSQL
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'biblioteca_db',
        'USER': 'biblioteca_user',
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

# Configuración de archivos estáticos y media
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

# Seguridad
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
Configuración de Gunicorn
# gunicorn.conf.py
bind = '0.0.0.0:8000'
workers = 3
timeout = 120
accesslog = '/var/log/gunicorn/access.log'
errorlog = '/var/log/gunicorn/error.log'
Configuración de Nginx
# /etc/nginx/sites-available/biblioteca
server {
    listen 80;
    server_name biblioteca.example.com www.biblioteca.example.com;

    location = /favicon.ico { access_log off; log_not_found off; }
    
    location /static/ {
        root /var/www/biblioteca;
    }

    location /media/ {
        root /var/www/biblioteca;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}
Checklist de despliegue:
  • Configurar variables de entorno
  • Realizar respaldo de base de datos
  • Recolectar archivos estáticos
  • Configurar HTTPS con Let's Encrypt
  • Implementar monitoreo y logs

Quiz: Proyecto CRUD Completo

Comprueba tu comprensión del proyecto completo respondiendo las siguientes preguntas:

1. ¿Cuál es la ventaja de dividir el proyecto en múltiples aplicaciones Django?
2. ¿Por qué usamos models.PROTECT en la relación ForeignKey del modelo Libro?
3. ¿Qué ventaja ofrece usar Class-Based Views en lugar de Function-Based Views?
4. ¿Cuál es el propósito del decorador @bibliotecario_required?
5. Al desplegar en producción, ¿por qué es importante configurar SECURE_SSL_REDIRECT = True?
Recuerda revisar cada sección del módulo si tienes dudas sobre alguna pregunta.