TODOS LOS CÓDIGOS COMPLETOS
Esta página contiene TODO el código sin omisiones. Navega por las pestañas y copia cada archivo.
📁 Crear carpetas: templates/base, templates/libros, static/css, static/js, media/portadas, media/autores
libros/views.py (217 líneas - 7 vistas)
from django.shortcuts import render, get_object_or_404
from django.db.models import Count, Avg, Q
from .models import Libro, Autor, Categoria, Editorial, Prestamo
def inicio(request):
"""Vista principal con estadísticas del sistema"""
total_libros = Libro.objects.count()
total_autores = Autor.objects.count()
total_categorias = Categoria.objects.filter(activa=True).count()
libros_disponibles = Libro.objects.filter(estado='disponible').count()
prestamos_activos = Prestamo.objects.filter(estado='activo').count()
libros_destacados = Libro.objects.filter(
calificacion__gte=4.0
).order_by('-calificacion')[:6]
ultimos_libros = Libro.objects.order_by('-fecha_publicacion')[:6]
context = {
'total_libros': total_libros,
'total_autores': total_autores,
'total_categorias': total_categorias,
'libros_disponibles': libros_disponibles,
'prestamos_activos': prestamos_activos,
'libros_destacados': libros_destacados,
'ultimos_libros': ultimos_libros,
}
return render(request, 'libros/inicio.html', context)
def lista_libros(request):
"""Listado de todos los libros con filtros"""
libros = Libro.objects.select_related('autor', 'editorial').prefetch_related('categorias')
# Filtros
categoria_id = request.GET.get('categoria')
autor_id = request.GET.get('autor')
estado = request.GET.get('estado')
busqueda = request.GET.get('q')
if categoria_id:
libros = libros.filter(categorias__id=categoria_id)
if autor_id:
libros = libros.filter(autor__id=autor_id)
if estado:
libros = libros.filter(estado=estado)
if busqueda:
libros = libros.filter(
Q(titulo__icontains=busqueda) |
Q(isbn__icontains=busqueda) |
Q(autor__nombre__icontains=busqueda)
)
# Para los selectores de filtro
categorias = Categoria.objects.filter(activa=True)
autores = Autor.objects.all().order_by('nombre')
estados = Libro.ESTADO_CHOICES
context = {
'libros': libros,
'categorias': categorias,
'autores': autores,
'estados': estados,
'categoria_seleccionada': categoria_id,
'autor_seleccionado': autor_id,
'estado_seleccionado': estado,
'busqueda': busqueda,
}
return render(request, 'libros/lista_libros.html', context)
def detalle_libro(request, id):
"""Detalles completos de un libro específico"""
libro = get_object_or_404(
Libro.objects.select_related('autor', 'editorial').prefetch_related('categorias'),
id=id
)
# Libros relacionados (mismo autor o categorías similares)
libros_relacionados = Libro.objects.filter(
Q(autor=libro.autor) | Q(categorias__in=libro.categorias.all())
).exclude(id=libro.id).distinct()[:4]
context = {
'libro': libro,
'libros_relacionados': libros_relacionados,
}
return render(request, 'libros/detalle_libro.html', context)
def lista_autores(request):
"""Listado de todos los autores con estadísticas"""
autores = Autor.objects.annotate(
total_libros=Count('libro'),
calificacion_promedio=Avg('libro__calificacion')
).order_by('nombre')
busqueda = request.GET.get('q')
if busqueda:
autores = autores.filter(
Q(nombre__icontains=busqueda) |
Q(pais_origen__icontains=busqueda)
)
context = {
'autores': autores,
'busqueda': busqueda,
}
return render(request, 'libros/lista_autores.html', context)
def detalle_autor(request, id):
"""Detalles de un autor y sus libros"""
autor = get_object_or_404(Autor, id=id)
libros = Libro.objects.filter(autor=autor).prefetch_related('categorias')
# Estadísticas del autor
total_libros = libros.count()
calificacion_promedio = libros.aggregate(Avg('calificacion'))['calificacion__avg'] or 0
context = {
'autor': autor,
'libros': libros,
'total_libros': total_libros,
'calificacion_promedio': round(calificacion_promedio, 1),
}
return render(request, 'libros/detalle_autor.html', context)
def busqueda_avanzada(request):
"""Búsqueda avanzada con múltiples filtros"""
libros = Libro.objects.select_related('autor', 'editorial').prefetch_related('categorias')
# Filtros avanzados
titulo = request.GET.get('titulo')
autor = request.GET.get('autor')
categoria = request.GET.get('categoria')
editorial = request.GET.get('editorial')
año_desde = request.GET.get('año_desde')
año_hasta = request.GET.get('año_hasta')
precio_min = request.GET.get('precio_min')
precio_max = request.GET.get('precio_max')
calificacion_min = request.GET.get('calificacion_min')
if titulo:
libros = libros.filter(titulo__icontains=titulo)
if autor:
libros = libros.filter(autor__nombre__icontains=autor)
if categoria:
libros = libros.filter(categorias__id=categoria)
if editorial:
libros = libros.filter(editorial__id=editorial)
if año_desde:
libros = libros.filter(fecha_publicacion__year__gte=año_desde)
if año_hasta:
libros = libros.filter(fecha_publicacion__year__lte=año_hasta)
if precio_min:
libros = libros.filter(precio__gte=precio_min)
if precio_max:
libros = libros.filter(precio__lte=precio_max)
if calificacion_min:
libros = libros.filter(calificacion__gte=calificacion_min)
categorias = Categoria.objects.filter(activa=True)
editoriales = Editorial.objects.all().order_by('nombre')
context = {
'libros': libros,
'categorias': categorias,
'editoriales': editoriales,
'filtros': request.GET,
}
return render(request, 'libros/busqueda_avanzada.html', context)
def estadisticas(request):
"""Dashboard con métricas y estadísticas del sistema"""
# Estadísticas generales
total_libros = Libro.objects.count()
total_autores = Autor.objects.count()
total_categorias = Categoria.objects.count()
total_editoriales = Editorial.objects.count()
# Estadísticas de libros
libros_por_estado = Libro.objects.values('estado').annotate(
total=Count('id')
).order_by('-total')
# Top 10 autores con más libros
top_autores = Autor.objects.annotate(
total_libros=Count('libro')
).order_by('-total_libros')[:10]
# Top 10 categorías más populares
top_categorias = Categoria.objects.annotate(
total_libros=Count('libro')
).order_by('-total_libros')[:10]
# Libros mejor calificados
mejores_libros = Libro.objects.filter(
calificacion__gte=4.0
).order_by('-calificacion')[:10]
# Estadísticas de préstamos
total_prestamos = Prestamo.objects.count()
prestamos_activos = Prestamo.objects.filter(estado='activo').count()
prestamos_retrasados = Prestamo.objects.filter(estado='retrasado').count()
context = {
'total_libros': total_libros,
'total_autores': total_autores,
'total_categorias': total_categorias,
'total_editoriales': total_editoriales,
'libros_por_estado': libros_por_estado,
'top_autores': top_autores,
'top_categorias': top_categorias,
'mejores_libros': mejores_libros,
'total_prestamos': total_prestamos,
'prestamos_activos': prestamos_activos,
'prestamos_retrasados': prestamos_retrasados,
}
return render(request, 'libros/estadisticas.html', context)
libros/urls.py
from django.urls import path
from . import views
app_name = 'libros'
urlpatterns = [
path('', views.inicio, name='inicio'),
path('libros/', views.lista_libros, name='lista_libros'),
path('libro/<int:id>/', views.detalle_libro, name='detalle_libro'),
path('autores/', views.lista_autores, name='lista_autores'),
path('autor/<int:id>/', views.detalle_autor, name='detalle_autor'),
path('busqueda/', views.busqueda_avanzada, name='busqueda_avanzada'),
path('estadisticas/', views.estadisticas, name='estadisticas'),
]
biblioteca_project/urls.py
"""
URL configuration for biblioteca_project project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('libros.urls')),
]
# Servir archivos media en desarrollo
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
templates/base/base.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Biblioteca Django{% endblock %}</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom CSS -->
{% load static %}
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{% url 'libros:inicio' %}">
<i class="fas fa-book-reader me-2"></i>
Biblioteca Django
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'libros:inicio' %}">
<i class="fas fa-home me-1"></i>Inicio
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'libros:lista_libros' %}">
<i class="fas fa-book me-1"></i>Libros
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'libros:lista_autores' %}">
<i class="fas fa-user-edit me-1"></i>Autores
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'libros:busqueda_avanzada' %}">
<i class="fas fa-search me-1"></i>Búsqueda Avanzada
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'libros:estadisticas' %}">
<i class="fas fa-chart-bar me-1"></i>Estadísticas
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="py-4">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="bg-dark text-white mt-5 py-4">
<div class="container">
<div class="row">
<div class="col-md-4">
<h5><i class="fas fa-book-reader me-2"></i>Biblioteca Django</h5>
<p class="text-muted">Sistema de gestión bibliotecaria completo desarrollado con Django.</p>
</div>
<div class="col-md-4">
<h5>Enlaces Rápidos</h5>
<ul class="list-unstyled">
<li><a href="{% url 'libros:lista_libros' %}" class="text-white-50 text-decoration-none">Catálogo de Libros</a></li>
<li><a href="{% url 'libros:lista_autores' %}" class="text-white-50 text-decoration-none">Autores</a></li>
<li><a href="{% url 'libros:estadisticas' %}" class="text-white-50 text-decoration-none">Estadísticas</a></li>
</ul>
</div>
<div class="col-md-4">
<h5>Contacto</h5>
<p class="text-muted">
<i class="fas fa-envelope me-2"></i>biblioteca@example.com<br>
<i class="fas fa-phone me-2"></i>+123 456 7890
</p>
</div>
</div>
<hr class="bg-secondary">
<div class="text-center text-muted">
<p>© 2026 Biblioteca Django. Todos los derechos reservados.</p>
</div>
</div>
</footer>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JS -->
<script src="{% static 'js/main.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
templates/libros/inicio.html
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Inicio - Biblioteca Django{% endblock %}
{% block content %}
<div class="container">
<!-- Hero Section -->
<div class="jumbotron bg-light p-5 rounded-3 mb-4">
<h1 class="display-4">
<i class="fas fa-book-reader text-primary"></i>
Bienvenido a Biblioteca Django
</h1>
<p class="lead">Sistema completo de gestión bibliotecaria con Django</p>
<hr class="my-4">
<p>Explora nuestro catálogo de libros, descubre nuevos autores y gestiona préstamos de manera eficiente.</p>
<a class="btn btn-primary btn-lg" href="{% url 'libros:lista_libros' %}" role="button">
<i class="fas fa-book me-2"></i>Ver Catálogo
</a>
</div>
<!-- Estadísticas Principales -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card text-white bg-primary h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">Total Libros</h5>
<h2 class="mb-0">{{ total_libros }}</h2>
</div>
<i class="fas fa-book fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-success h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">Autores</h5>
<h2 class="mb-0">{{ total_autores }}</h2>
</div>
<i class="fas fa-user-edit fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-info h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">Disponibles</h5>
<h2 class="mb-0">{{ libros_disponibles }}</h2>
</div>
<i class="fas fa-check-circle fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-warning h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">Préstamos</h5>
<h2 class="mb-0">{{ prestamos_activos }}</h2>
</div>
<i class="fas fa-exchange-alt fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Libros Destacados -->
<section class="mb-5">
<h2 class="mb-4">
<i class="fas fa-star text-warning"></i> Libros Destacados
</h2>
<div class="row">
{% for libro in libros_destacados %}
<div class="col-md-4 col-lg-2 mb-4">
<div class="card h-100 shadow-sm libro-card">
{% if libro.portada %}
<img src="{{ libro.portada.url }}" class="card-img-top" alt="{{ libro.titulo }}">
{% else %}
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 200px;">
<i class="fas fa-book fa-3x text-white"></i>
</div>
{% endif %}
<div class="card-body">
<h6 class="card-title">{{ libro.titulo|truncatewords:5 }}</h6>
<p class="card-text text-muted small">{{ libro.autor.nombre }}</p>
<div class="mb-2">
<span class="badge bg-warning text-dark">
<i class="fas fa-star"></i> {{ libro.calificacion }}
</span>
</div>
<a href="{% url 'libros:detalle_libro' libro.id %}" class="btn btn-sm btn-primary w-100">
Ver Detalles
</a>
</div>
</div>
</div>
{% empty %}
<p class="text-muted">No hay libros destacados disponibles.</p>
{% endfor %}
</div>
</section>
<!-- Últimos Libros Agregados -->
<section>
<h2 class="mb-4">
<i class="fas fa-clock text-info"></i> Últimos Libros Agregados
</h2>
<div class="row">
{% for libro in ultimos_libros %}
<div class="col-md-4 col-lg-2 mb-4">
<div class="card h-100 shadow-sm libro-card">
{% if libro.portada %}
<img src="{{ libro.portada.url }}" class="card-img-top" alt="{{ libro.titulo }}">
{% else %}
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 200px;">
<i class="fas fa-book fa-3x text-white"></i>
</div>
{% endif %}
<div class="card-body">
<h6 class="card-title">{{ libro.titulo|truncatewords:5 }}</h6>
<p class="card-text text-muted small">{{ libro.autor.nombre }}</p>
<div class="mb-2">
<span class="badge bg-info">
{{ libro.fecha_publicacion|date:"Y" }}
</span>
</div>
<a href="{% url 'libros:detalle_libro' libro.id %}" class="btn btn-sm btn-primary w-100">
Ver Detalles
</a>
</div>
</div>
</div>
{% empty %}
<p class="text-muted">No hay libros disponibles.</p>
{% endfor %}
</div>
</section>
</div>
{% endblock %}
templates/libros/lista_libros.html
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Catálogo de Libros - Biblioteca Django{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4">
<i class="fas fa-book text-primary"></i> Catálogo de Libros
</h1>
<!-- Barra de Búsqueda y Filtros -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" action="{% url 'libros:lista_libros' %}">
<div class="row g-3">
<div class="col-md-4">
<label for="search" class="form-label">Buscar</label>
<input type="text" class="form-control" id="search" name="q"
placeholder="Título, ISBN o Autor..." value="{{ busqueda }}">
</div>
<div class="col-md-3">
<label for="categoria" class="form-label">Categoría</label>
<select class="form-select" id="categoria" name="categoria">
<option value="">Todas las categorías</option>
{% for cat in categorias %}
<option value="{{ cat.id }}" {% if categoria_seleccionada == cat.id|stringformat:"s" %}selected{% endif %}>
{{ cat.nombre }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="autor" class="form-label">Autor</label>
<select class="form-select" id="autor" name="autor">
<option value="">Todos los autores</option>
{% for autor in autores %}
<option value="{{ autor.id }}" {% if autor_seleccionado == autor.id|stringformat:"s" %}selected{% endif %}>
{{ autor.nombre }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="estado" class="form-label">Estado</label>
<select class="form-select" id="estado" name="estado">
<option value="">Todos</option>
{% for estado_value, estado_label in estados %}
<option value="{{ estado_value }}" {% if estado_seleccionado == estado_value %}selected{% endif %}>
{{ estado_label }}
</option>
{% endfor %}
</select>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i> Buscar
</button>
<a href="{% url 'libros:lista_libros' %}" class="btn btn-secondary">
<i class="fas fa-redo"></i> Limpiar
</a>
</div>
</div>
</form>
</div>
</div>
<!-- Resultados -->
<div class="mb-3">
<p class="text-muted">
<i class="fas fa-info-circle"></i>
Se encontraron <strong>{{ libros.count }}</strong> libro(s)
</p>
</div>
<div class="row">
{% for libro in libros %}
<div class="col-md-6 col-lg-3 mb-4">
<div class="card h-100 shadow-sm libro-card">
{% if libro.portada %}
<img src="{{ libro.portada.url }}" class="card-img-top" alt="{{ libro.titulo }}" style="height: 250px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 250px;">
<i class="fas fa-book fa-4x text-white"></i>
</div>
{% endif %}
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ libro.titulo|truncatewords:6 }}</h5>
<p class="card-text text-muted">
<i class="fas fa-user"></i> {{ libro.autor.nombre }}
</p>
<p class="card-text small">
<i class="fas fa-building"></i> {{ libro.editorial.nombre }}
</p>
<div class="mb-2">
{% for categoria in libro.categorias.all|slice:":2" %}
<span class="badge bg-secondary">{{ categoria.nombre }}</span>
{% endfor %}
</div>
<div class="mb-2">
<span class="badge bg-warning text-dark">
<i class="fas fa-star"></i> {{ libro.calificacion }}
</span>
{% if libro.estado == 'disponible' %}
<span class="badge bg-success">Disponible</span>
{% elif libro.estado == 'prestado' %}
<span class="badge bg-danger">Prestado</span>
{% else %}
<span class="badge bg-warning">Mantenimiento</span>
{% endif %}
</div>
<div class="mb-2">
<strong class="text-primary">${{ libro.precio }}</strong>
</div>
<div class="mt-auto">
<a href="{% url 'libros:detalle_libro' libro.id %}" class="btn btn-primary btn-sm w-100">
<i class="fas fa-eye"></i> Ver Detalles
</a>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> No se encontraron libros con los criterios especificados.
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
templates/libros/detalle_libro.html
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{{ libro.titulo }} - Biblioteca Django{% endblock %}
{% block content %}
<div class="container">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'libros:inicio' %}">Inicio</a></li>
<li class="breadcrumb-item"><a href="{% url 'libros:lista_libros' %}">Libros</a></li>
<li class="breadcrumb-item active">{{ libro.titulo }}</li>
</ol>
</nav>
<div class="row">
<!-- Portada del Libro -->
<div class="col-md-4 mb-4">
<div class="card">
{% if libro.portada %}
<img src="{{ libro.portada.url }}" class="card-img-top" alt="{{ libro.titulo }}">
{% else %}
<div class="bg-secondary d-flex align-items-center justify-content-center" style="height: 400px;">
<i class="fas fa-book fa-5x text-white"></i>
</div>
{% endif %}
<div class="card-body">
<h4 class="text-center text-primary">${{ libro.precio }}</h4>
<div class="d-grid gap-2">
{% if libro.estado == 'disponible' %}
<button class="btn btn-success" disabled>
<i class="fas fa-check-circle"></i> Disponible
</button>
{% elif libro.estado == 'prestado' %}
<button class="btn btn-danger" disabled>
<i class="fas fa-times-circle"></i> Prestado
</button>
{% else %}
<button class="btn btn-warning" disabled>
<i class="fas fa-tools"></i> En Mantenimiento
</button>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Información del Libro -->
<div class="col-md-8">
<h1 class="mb-3">{{ libro.titulo }}</h1>
<div class="mb-3">
<span class="badge bg-warning text-dark fs-6">
<i class="fas fa-star"></i> {{ libro.calificacion }} / 5.0
</span>
<span class="badge bg-info fs-6">
<i class="fas fa-warehouse"></i> Stock: {{ libro.stock }}
</span>
</div>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Información General</h5>
<table class="table table-borderless">
<tbody>
<tr>
<th width="30%"><i class="fas fa-user text-primary"></i> Autor:</th>
<td>
<a href="{% url 'libros:detalle_autor' libro.autor.id %}">
{{ libro.autor.nombre }}
</a>
</td>
</tr>
<tr>
<th><i class="fas fa-building text-primary"></i> Editorial:</th>
<td>{{ libro.editorial.nombre }}</td>
</tr>
<tr>
<th><i class="fas fa-barcode text-primary"></i> ISBN:</th>
<td>{{ libro.isbn }}</td>
</tr>
<tr>
<th><i class="fas fa-calendar text-primary"></i> Publicación:</th>
<td>{{ libro.fecha_publicacion|date:"d/m/Y" }}</td>
</tr>
<tr>
<th><i class="fas fa-file-alt text-primary"></i> Páginas:</th>
<td>{{ libro.numero_paginas }} páginas</td>
</tr>
<tr>
<th><i class="fas fa-tags text-primary"></i> Categorías:</th>
<td>
{% for categoria in libro.categorias.all %}
<span class="badge bg-secondary">{{ categoria.nombre }}</span>
{% endfor %}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title"><i class="fas fa-align-left text-primary"></i> Sinopsis</h5>
<p class="card-text">{{ libro.sinopsis }}</p>
</div>
</div>
</div>
</div>
<!-- Libros Relacionados -->
{% if libros_relacionados %}
<section class="mt-5">
<h3 class="mb-4">
<i class="fas fa-book-open text-primary"></i> Libros Relacionados
</h3>
<div class="row">
{% for libro_rel in libros_relacionados %}
<div class="col-md-3 mb-4">
<div class="card h-100 shadow-sm libro-card">
{% if libro_rel.portada %}
<img src="{{ libro_rel.portada.url }}" class="card-img-top" alt="{{ libro_rel.titulo }}" style="height: 200px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 200px;">
<i class="fas fa-book fa-3x text-white"></i>
</div>
{% endif %}
<div class="card-body">
<h6 class="card-title">{{ libro_rel.titulo|truncatewords:5 }}</h6>
<p class="card-text text-muted small">{{ libro_rel.autor.nombre }}</p>
<a href="{% url 'libros:detalle_libro' libro_rel.id %}" class="btn btn-sm btn-primary w-100">
Ver Detalles
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
</div>
{% endblock %}
templates/libros/lista_autores.html
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Autores - Biblioteca Django{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4">
<i class="fas fa-user-edit text-primary"></i> Directorio de Autores
</h1>
<!-- Barra de Búsqueda -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" action="{% url 'libros:lista_autores' %}">
<div class="row g-3">
<div class="col-md-10">
<input type="text" class="form-control" name="q"
placeholder="Buscar autor por nombre o país..." value="{{ busqueda }}">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search"></i> Buscar
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Resultados -->
<div class="mb-3">
<p class="text-muted">
<i class="fas fa-info-circle"></i>
Total de autores: <strong>{{ autores.count }}</strong>
</p>
</div>
<div class="row">
{% for autor in autores %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 shadow-sm autor-card">
<div class="card-body">
<div class="d-flex align-items-start">
<div class="flex-shrink-0">
{% if autor.foto %}
<img src="{{ autor.foto.url }}" class="rounded-circle"
style="width: 80px; height: 80px; object-fit: cover;" alt="{{ autor.nombre }}">
{% else %}
<div class="rounded-circle bg-primary d-flex align-items-center justify-content-center"
style="width: 80px; height: 80px;">
<i class="fas fa-user fa-2x text-white"></i>
</div>
{% endif %}
</div>
<div class="flex-grow-1 ms-3">
<h5 class="card-title mb-2">{{ autor.nombre }}</h5>
<p class="card-text text-muted mb-2">
<i class="fas fa-globe"></i> {{ autor.pais_origen }}
</p>
<p class="card-text text-muted mb-2">
<i class="fas fa-birthday-cake"></i> {{ autor.fecha_nacimiento|date:"d/m/Y" }}
</p>
<div class="mb-2">
<span class="badge bg-primary">
<i class="fas fa-book"></i> {{ autor.total_libros }} libro(s)
</span>
{% if autor.calificacion_promedio %}
<span class="badge bg-warning text-dark">
<i class="fas fa-star"></i> {{ autor.calificacion_promedio|floatformat:1 }}
</span>
{% endif %}
</div>
<p class="card-text small">{{ autor.biografia|truncatewords:20 }}</p>
<a href="{% url 'libros:detalle_autor' autor.id %}" class="btn btn-primary btn-sm">
<i class="fas fa-eye"></i> Ver Perfil
</a>
</div>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> No se encontraron autores.
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
templates/libros/detalle_autor.html
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{{ autor.nombre }} - Biblioteca Django{% endblock %}
{% block content %}
<div class="container">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'libros:inicio' %}">Inicio</a></li>
<li class="breadcrumb-item"><a href="{% url 'libros:lista_autores' %}">Autores</a></li>
<li class="breadcrumb-item active">{{ autor.nombre }}</li>
</ol>
</nav>
<!-- Perfil del Autor -->
<div class="row mb-4">
<div class="col-md-3">
{% if autor.foto %}
<img src="{{ autor.foto.url }}" class="img-fluid rounded-circle shadow" alt="{{ autor.nombre }}">
{% else %}
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center mx-auto shadow"
style="width: 200px; height: 200px;">
<i class="fas fa-user fa-5x text-white"></i>
</div>
{% endif %}
</div>
<div class="col-md-9">
<h1 class="mb-3">{{ autor.nombre }}</h1>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Información Personal</h5>
<table class="table table-borderless mb-0">
<tbody>
<tr>
<th width="30%"><i class="fas fa-birthday-cake text-primary"></i> Fecha de Nacimiento:</th>
<td>{{ autor.fecha_nacimiento|date:"d/m/Y" }}</td>
</tr>
<tr>
<th><i class="fas fa-globe text-primary"></i> País de Origen:</th>
<td>{{ autor.pais_origen }}</td>
</tr>
<tr>
<th><i class="fas fa-book text-primary"></i> Total de Libros:</th>
<td><span class="badge bg-primary">{{ total_libros }}</span></td>
</tr>
<tr>
<th><i class="fas fa-star text-primary"></i> Calificación Promedio:</th>
<td>
<span class="badge bg-warning text-dark">{{ calificacion_promedio }} / 5.0</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title"><i class="fas fa-align-left text-primary"></i> Biografía</h5>
<p class="card-text">{{ autor.biografia }}</p>
</div>
</div>
</div>
</div>
<!-- Libros del Autor -->
<section class="mt-5">
<h3 class="mb-4">
<i class="fas fa-book-open text-primary"></i> Obras de {{ autor.nombre }}
</h3>
{% if libros %}
<div class="row">
{% for libro in libros %}
<div class="col-md-6 col-lg-3 mb-4">
<div class="card h-100 shadow-sm libro-card">
{% if libro.portada %}
<img src="{{ libro.portada.url }}" class="card-img-top" alt="{{ libro.titulo }}"
style="height: 250px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center"
style="height: 250px;">
<i class="fas fa-book fa-4x text-white"></i>
</div>
{% endif %}
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ libro.titulo|truncatewords:6 }}</h5>
<p class="card-text text-muted small">
<i class="fas fa-calendar"></i> {{ libro.fecha_publicacion|date:"Y" }}
</p>
<div class="mb-2">
{% for categoria in libro.categorias.all|slice:":2" %}
<span class="badge bg-secondary">{{ categoria.nombre }}</span>
{% endfor %}
</div>
<div class="mb-2">
<span class="badge bg-warning text-dark">
<i class="fas fa-star"></i> {{ libro.calificacion }}
</span>
{% if libro.estado == 'disponible' %}
<span class="badge bg-success">Disponible</span>
{% elif libro.estado == 'prestado' %}
<span class="badge bg-danger">Prestado</span>
{% else %}
<span class="badge bg-warning">Mantenimiento</span>
{% endif %}
</div>
<div class="mb-2">
<strong class="text-primary">${{ libro.precio }}</strong>
</div>
<div class="mt-auto">
<a href="{% url 'libros:detalle_libro' libro.id %}" class="btn btn-primary btn-sm w-100">
<i class="fas fa-eye"></i> Ver Detalles
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> Este autor aún no tiene libros registrados en la biblioteca.
</div>
{% endif %}
</section>
</div>
{% endblock %}
templates/libros/busqueda_avanzada.html
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Búsqueda Avanzada - Biblioteca Django{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4">
<i class="fas fa-search text-primary"></i> Búsqueda Avanzada
</h1>
<!-- Formulario de Búsqueda Avanzada -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-filter"></i> Filtros de Búsqueda</h5>
</div>
<div class="card-body">
<form method="GET" action="{% url 'libros:busqueda_avanzada' %}">
<div class="row g-3">
<!-- Título -->
<div class="col-md-6">
<label for="titulo" class="form-label">Título</label>
<input type="text" class="form-control" id="titulo" name="titulo"
placeholder="Buscar por título..." value="{{ filtros.titulo }}">
</div>
<!-- Autor -->
<div class="col-md-6">
<label for="autor" class="form-label">Autor</label>
<input type="text" class="form-control" id="autor" name="autor"
placeholder="Nombre del autor..." value="{{ filtros.autor }}">
</div>
<!-- Categoría -->
<div class="col-md-4">
<label for="categoria" class="form-label">Categoría</label>
<select class="form-select" id="categoria" name="categoria">
<option value="">Todas las categorías</option>
{% for cat in categorias %}
<option value="{{ cat.id }}" {% if filtros.categoria == cat.id|stringformat:"s" %}selected{% endif %}>
{{ cat.nombre }}
</option>
{% endfor %}
</select>
</div>
<!-- Editorial -->
<div class="col-md-4">
<label for="editorial" class="form-label">Editorial</label>
<select class="form-select" id="editorial" name="editorial">
<option value="">Todas las editoriales</option>
{% for edit in editoriales %}
<option value="{{ edit.id }}" {% if filtros.editorial == edit.id|stringformat:"s" %}selected{% endif %}>
{{ edit.nombre }}
</option>
{% endfor %}
</select>
</div>
<!-- Calificación Mínima -->
<div class="col-md-4">
<label for="calificacion_min" class="form-label">Calificación Mínima</label>
<select class="form-select" id="calificacion_min" name="calificacion_min">
<option value="">Todas</option>
<option value="5.0" {% if filtros.calificacion_min == "5.0" %}selected{% endif %}>5 Estrellas</option>
<option value="4.0" {% if filtros.calificacion_min == "4.0" %}selected{% endif %}>4+ Estrellas</option>
<option value="3.0" {% if filtros.calificacion_min == "3.0" %}selected{% endif %}>3+ Estrellas</option>
<option value="2.0" {% if filtros.calificacion_min == "2.0" %}selected{% endif %}>2+ Estrellas</option>
</select>
</div>
<!-- Año Desde -->
<div class="col-md-3">
<label for="año_desde" class="form-label">Año Desde</label>
<input type="number" class="form-control" id="año_desde" name="año_desde"
placeholder="2000" min="1900" max="2026" value="{{ filtros.año_desde }}">
</div>
<!-- Año Hasta -->
<div class="col-md-3">
<label for="año_hasta" class="form-label">Año Hasta</label>
<input type="number" class="form-control" id="año_hasta" name="año_hasta"
placeholder="2026" min="1900" max="2026" value="{{ filtros.año_hasta }}">
</div>
<!-- Precio Mínimo -->
<div class="col-md-3">
<label for="precio_min" class="form-label">Precio Mínimo ($)</label>
<input type="number" class="form-control" id="precio_min" name="precio_min"
placeholder="0" step="0.01" min="0" value="{{ filtros.precio_min }}">
</div>
<!-- Precio Máximo -->
<div class="col-md-3">
<label for="precio_max" class="form-label">Precio Máximo ($)</label>
<input type="number" class="form-control" id="precio_max" name="precio_max"
placeholder="1000" step="0.01" min="0" value="{{ filtros.precio_max }}">
</div>
<!-- Botones -->
<div class="col-12">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i> Buscar
</button>
<a href="{% url 'libros:busqueda_avanzada' %}" class="btn btn-secondary">
<i class="fas fa-redo"></i> Limpiar Filtros
</a>
</div>
</div>
</form>
</div>
</div>
<!-- Resultados -->
<div class="mb-3">
<h4>
<i class="fas fa-list"></i> Resultados de la Búsqueda
<span class="badge bg-primary">{{ libros.count }} libro(s)</span>
</h4>
</div>
<div class="row">
{% for libro in libros %}
<div class="col-md-6 col-lg-3 mb-4">
<div class="card h-100 shadow-sm libro-card">
{% if libro.portada %}
<img src="{{ libro.portada.url }}" class="card-img-top" alt="{{ libro.titulo }}"
style="height: 250px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center"
style="height: 250px;">
<i class="fas fa-book fa-4x text-white"></i>
</div>
{% endif %}
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ libro.titulo|truncatewords:6 }}</h5>
<p class="card-text text-muted">
<i class="fas fa-user"></i> {{ libro.autor.nombre }}
</p>
<p class="card-text small">
<i class="fas fa-calendar"></i> {{ libro.fecha_publicacion|date:"Y" }}
</p>
<div class="mb-2">
<span class="badge bg-warning text-dark">
<i class="fas fa-star"></i> {{ libro.calificacion }}
</span>
</div>
<div class="mb-2">
<strong class="text-primary">${{ libro.precio }}</strong>
</div>
<div class="mt-auto">
<a href="{% url 'libros:detalle_libro' libro.id %}" class="btn btn-primary btn-sm w-100">
<i class="fas fa-eye"></i> Ver Detalles
</a>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> No se encontraron libros con los criterios especificados.
Intenta ajustar los filtros de búsqueda.
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
templates/libros/estadisticas.html
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Estadísticas - Biblioteca Django{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4">
<i class="fas fa-chart-bar text-primary"></i> Dashboard de Estadísticas
</h1>
<!-- Estadísticas Generales -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card text-white bg-primary h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title">Total Libros</h6>
<h2 class="mb-0">{{ total_libros }}</h2>
</div>
<i class="fas fa-book fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-success h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title">Autores</h6>
<h2 class="mb-0">{{ total_autores }}</h2>
</div>
<i class="fas fa-user-edit fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-info h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title">Categorías</h6>
<h2 class="mb-0">{{ total_categorias }}</h2>
</div>
<i class="fas fa-tags fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-warning h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title">Editoriales</h6>
<h2 class="mb-0">{{ total_editoriales }}</h2>
</div>
<i class="fas fa-building fa-3x opacity-50"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Libros por Estado -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-chart-pie"></i> Libros por Estado</h5>
</div>
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>Estado</th>
<th>Cantidad</th>
<th>Porcentaje</th>
</tr>
</thead>
<tbody>
{% for item in libros_por_estado %}
<tr>
<td>
{% if item.estado == 'disponible' %}
<span class="badge bg-success">Disponible</span>
{% elif item.estado == 'prestado' %}
<span class="badge bg-danger">Prestado</span>
{% else %}
<span class="badge bg-warning">Mantenimiento</span>
{% endif %}
</td>
<td><strong>{{ item.total }}</strong></td>
<td>
{% widthratio item.total total_libros 100 %}%
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Estadísticas de Préstamos -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="fas fa-exchange-alt"></i> Estadísticas de Préstamos</h5>
</div>
<div class="card-body">
<table class="table table-hover">
<tbody>
<tr>
<th>Total Préstamos</th>
<td><span class="badge bg-primary fs-5">{{ total_prestamos }}</span></td>
</tr>
<tr>
<th>Préstamos Activos</th>
<td><span class="badge bg-success fs-5">{{ prestamos_activos }}</span></td>
</tr>
<tr>
<th>Préstamos Retrasados</th>
<td><span class="badge bg-danger fs-5">{{ prestamos_retrasados }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Top 10 Autores -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="fas fa-trophy"></i> Top 10 Autores (por cantidad de libros)</h5>
</div>
<div class="card-body">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>#</th>
<th>Autor</th>
<th>Libros</th>
</tr>
</thead>
<tbody>
{% for autor in top_autores %}
<tr>
<td>{{ forloop.counter }}</td>
<td>
<a href="{% url 'libros:detalle_autor' autor.id %}">
{{ autor.nombre }}
</a>
</td>
<td><span class="badge bg-primary">{{ autor.total_libros }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Top 10 Categorías -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="fas fa-star"></i> Top 10 Categorías Más Populares</h5>
</div>
<div class="card-body">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>#</th>
<th>Categoría</th>
<th>Libros</th>
</tr>
</thead>
<tbody>
{% for categoria in top_categorias %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ categoria.nombre }}</td>
<td><span class="badge bg-warning text-dark">{{ categoria.total_libros }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Libros Mejor Calificados -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-danger text-white">
<h5 class="mb-0"><i class="fas fa-star"></i> Top 10 Libros Mejor Calificados</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>#</th>
<th>Título</th>
<th>Autor</th>
<th>Editorial</th>
<th>Calificación</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for libro in mejores_libros %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ libro.titulo }}</td>
<td>
<a href="{% url 'libros:detalle_autor' libro.autor.id %}">
{{ libro.autor.nombre }}
</a>
</td>
<td>{{ libro.editorial.nombre }}</td>
<td>
<span class="badge bg-warning text-dark">
<i class="fas fa-star"></i> {{ libro.calificacion }}
</span>
</td>
<td>
{% if libro.estado == 'disponible' %}
<span class="badge bg-success">Disponible</span>
{% elif libro.estado == 'prestado' %}
<span class="badge bg-danger">Prestado</span>
{% else %}
<span class="badge bg-warning">Mantenimiento</span>
{% endif %}
</td>
<td>
<a href="{% url 'libros:detalle_libro' libro.id %}" class="btn btn-sm btn-primary">
<i class="fas fa-eye"></i> Ver
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
static/css/styles.css (284 líneas)
/* ===================================
BIBLIOTECA DJANGO - ESTILOS PERSONALIZADOS
=================================== */
:root {
--primary-color: #0d6efd;
--secondary-color: #6c757d;
--success-color: #198754;
--info-color: #0dcaf0;
--warning-color: #ffc107;
--danger-color: #dc3545;
--dark-color: #212529;
--light-color: #f8f9fa;
}
/* General */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f5f5;
}
/* Navbar */
.navbar-brand {
font-weight: bold;
font-size: 1.5rem;
transition: all 0.3s ease;
}
.navbar-brand:hover {
transform: scale(1.05);
}
.nav-link {
transition: all 0.3s ease;
position: relative;
}
.nav-link::after {
content: '';
position: absolute;
width: 0;
height: 2px;
bottom: 0;
left: 50%;
background-color: white;
transition: all 0.3s ease;
transform: translateX(-50%);
}
.nav-link:hover::after {
width: 80%;
}
/* Cards */
.card {
border: none;
border-radius: 10px;
transition: all 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
/* Libro Cards */
.libro-card {
transition: all 0.3s ease;
overflow: hidden;
}
.libro-card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
}
.libro-card img {
transition: all 0.3s ease;
}
.libro-card:hover img {
transform: scale(1.1);
}
/* Autor Cards */
.autor-card {
transition: all 0.3s ease;
}
.autor-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.15);
}
/* Jumbotron */
.jumbotron {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.jumbotron h1 {
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
/* Buttons */
.btn {
border-radius: 5px;
transition: all 0.3s ease;
font-weight: 500;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* Badges */
.badge {
padding: 0.5em 0.8em;
font-weight: 500;
}
/* Tables */
.table {
background-color: white;
}
.table-hover tbody tr {
transition: all 0.3s ease;
}
.table-hover tbody tr:hover {
background-color: rgba(13, 110, 253, 0.05);
transform: scale(1.01);
}
/* Forms */
.form-control, .form-select {
border-radius: 5px;
border: 1px solid #dee2e6;
transition: all 0.3s ease;
}
.form-control:focus, .form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
/* Footer */
footer {
margin-top: auto;
}
footer a:hover {
color: white !important;
transition: color 0.3s ease;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease;
}
/* Loading Spinner */
.spinner-border {
width: 3rem;
height: 3rem;
}
/* Breadcrumb */
.breadcrumb {
background-color: transparent;
padding: 0.75rem 0;
}
.breadcrumb-item a {
color: var(--primary-color);
text-decoration: none;
}
.breadcrumb-item a:hover {
text-decoration: underline;
}
/* Rounded Images */
img.rounded-circle {
object-fit: cover;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #0b5ed7;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.jumbotron h1 {
font-size: 2rem;
}
.navbar-brand {
font-size: 1.2rem;
}
}
/* Opacity utilities */
.opacity-50 {
opacity: 0.5;
}
/* Card header custom */
.card-header {
border-bottom: 3px solid rgba(0,0,0,0.1);
font-weight: bold;
}
/* Image placeholders */
.card-img-top {
background-color: #e9ecef;
}
/* Text utilities */
.text-muted {
color: #6c757d !important;
}
/* Shadow utilities */
.shadow-sm {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
}
.shadow {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
/* Success alert */
.alert {
border-radius: 10px;
border: none;
}
/* Custom spacing */
.mb-custom {
margin-bottom: 2rem;
}
.mt-custom {
margin-top: 2rem;
}
/* Stats cards animation */
.card.text-white {
transition: all 0.3s ease;
}
.card.text-white:hover {
transform: scale(1.05);
}
static/js/main.js (238 líneas)
// ===================================
// BIBLIOTECA DJANGO - JAVASCRIPT PERSONALIZADO
// ===================================
// Esperar a que el DOM esté completamente cargado
document.addEventListener('DOMContentLoaded', function() {
// Inicializar tooltips de Bootstrap
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Inicializar popovers de Bootstrap
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
// Añadir animación de fade-in a las tarjetas
const cards = document.querySelectorAll('.card');
cards.forEach((card, index) => {
card.style.animation = `fadeIn 0.5s ease ${index * 0.1}s both`;
});
// Confirmación antes de eliminar (si se implementa)
const deleteButtons = document.querySelectorAll('.btn-delete');
deleteButtons.forEach(button => {
button.addEventListener('click', function(e) {
if (!confirm('¿Estás seguro de que deseas eliminar este elemento?')) {
e.preventDefault();
}
});
});
// Búsqueda en tiempo real (opcional)
const searchInput = document.querySelector('#search');
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
// Aquí se podría implementar búsqueda AJAX
console.log('Buscando:', this.value);
}, 500);
});
}
// Validación de formularios
const forms = document.querySelectorAll('.needs-validation');
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
// Scroll suave para enlaces internos
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
const target = document.querySelector(this.getAttribute('href'));
if (target) {
e.preventDefault();
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Botón de volver arriba
const backToTopButton = createBackToTopButton();
document.body.appendChild(backToTopButton);
window.addEventListener('scroll', () => {
if (window.pageYOffset > 300) {
backToTopButton.style.display = 'block';
} else {
backToTopButton.style.display = 'none';
}
});
backToTopButton.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
// Contador animado para estadísticas
animateCounters();
// Lazy loading para imágenes
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});
}
// Highlight active nav link
highlightActiveNavLink();
});
// Función para crear botón de volver arriba
function createBackToTopButton() {
const button = document.createElement('button');
button.innerHTML = '<i class="fas fa-arrow-up"></i>';
button.className = 'btn btn-primary';
button.style.cssText = `
position: fixed;
bottom: 30px;
right: 30px;
display: none;
z-index: 1000;
border-radius: 50%;
width: 50px;
height: 50px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
`;
button.setAttribute('title', 'Volver arriba');
return button;
}
// Función para animar contadores
function animateCounters() {
const counters = document.querySelectorAll('.card-body h2');
const speed = 200;
counters.forEach(counter => {
const updateCount = () => {
const target = +counter.innerText;
const count = +counter.getAttribute('data-count') || 0;
const increment = target / speed;
if (count < target) {
counter.setAttribute('data-count', Math.ceil(count + increment));
counter.innerText = Math.ceil(count + increment);
setTimeout(updateCount, 1);
} else {
counter.innerText = target;
}
};
// Solo animar si es un número
if (!isNaN(counter.innerText)) {
const originalValue = counter.innerText;
counter.innerText = '0';
counter.setAttribute('data-count', '0');
// Usar Intersection Observer para animar cuando sea visible
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
counter.innerText = originalValue;
observer.unobserve(counter);
}
});
});
observer.observe(counter);
}
});
}
// Función para resaltar el enlace activo en la navegación
function highlightActiveNavLink() {
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll('.navbar-nav .nav-link');
navLinks.forEach(link => {
if (link.getAttribute('href') === currentPath) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
}
// Función de utilidad para mostrar mensajes toast
function showToast(message, type = 'info') {
const toastHTML = `
<div class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
// Crear contenedor de toasts si no existe
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3';
document.body.appendChild(toastContainer);
}
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
const toastElement = toastContainer.lastElementChild;
const toast = new bootstrap.Toast(toastElement);
toast.show();
// Eliminar el toast después de que se oculte
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
// Función para formatear números con separadores de miles
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// Exportar funciones para uso global
window.bibliotecaApp = {
showToast,
formatNumber
};
console.log('Biblioteca Django - Sistema cargado correctamente ✓');
biblioteca_project/settings.py (Actualizaciones)
# Actualizar TEMPLATES (línea ~60):
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # <--- AGREGAR ESTA LÍNEA
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# Actualizar STATIC y MEDIA (al final del archivo):
STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
INSTRUCCIONES DE EJECUCIÓN
Paso a paso:
- Activar entorno virtual:
.\venv\Scripts\activate
- Ejecutar migraciones:
python manage.py makemigrations python manage.py migrate
- Crear superusuario:
python manage.py createsuperuser
- Ejecutar servidor:
python manage.py runserver
- Visitar: http://127.0.0.1:8000/
URLs Disponibles:
/- Página principal/libros/- Catálogo/libro/<id>/- Detalle libro/autores/- Autores/autor/<id>/- Detalle autor/busqueda/- Búsqueda avanzada/estadisticas/- Dashboard/admin/- Panel admin