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.
Antes de empezar a codificar, es importante planificar la estructura y funcionalidades de nuestra aplicación.
Módulo | Funcionalidades |
---|---|
Gestión de Libros |
|
Gestión de Usuarios |
|
Sistema de Préstamos |
|
Organizaremos nuestro proyecto en múltiples aplicaciones Django para mantener el código modular y mantenible.
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/
Implementaremos los modelos necesarios para nuestra aplicación de biblioteca.
# 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)])
# 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}"
Implementaremos las vistas necesarias para manejar las operaciones CRUD y los formularios para la entrada de datos.
# 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)
# 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'
# 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')
Crearemos templates reutilizables y un frontend moderno usando Bootstrap.
<!-- templates/base.html -->
{% raw %}
{% load static %}
{% block title %}Biblioteca{% endblock %}
{% if messages %}
{% for message in messages %}
{% endraw %}
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
<!-- 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 %}
{% endif %}
{{ libro.titulo }}
Por {{ libro.autor.nombre }}
{{ libro.descripcion|truncatewords:30 }}
{% empty %}
No se encontraron libros.
{% endfor %}
{% endblock %}
{% endraw %}
{% raw %}{% endraw %}
para mostrar correctamente la sintaxis de Django en los ejemplosImplementaremos un sistema de autenticación y autorización robusto para controlar el acceso a las diferentes funcionalidades.
# 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])
# 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})
Preparar y desplegar una aplicación Django en producción requiere varias consideraciones importantes.
# 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
# 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'
# /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;
}
}
Comprueba tu comprensión del proyecto completo respondiendo las siguientes preguntas: