Delete Plataform_Web directory

This commit is contained in:
Adrian Gonzalez Vega 2026-08-03 21:35:15 +02:00 committed by GitHub
parent 4b58b1ec97
commit 5f81e54793
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 0 additions and 3709 deletions

View file

@ -1,39 +0,0 @@
from django.contrib import admin
from .models import Documento, Factura
@admin.register(Documento)
class DocumentoAdmin(admin.ModelAdmin):
# Columnas principales
list_display = ('nombre', 'categoria', 'tipo', 'subido_por', 'fecha_subida')
# Filtros por área técnica, tipo de documento y temporada del documento
list_filter = ('categoria', 'tipo', 'temporada')
# Buscador de documentos por su nombre o descripción
search_fields = ('nombre', 'descripcion')
# Para asignar a que temporada pertenece el documento
filter_horizontal = ('temporada',)
# Automatización: para rellenar el creador del nuevo documento de manera automática
def save_model(self, request, obj, form, change):
if not obj.subido_por:
obj.subido_por = request.user
super().save_model(request, obj, form, change)
@admin.register(Factura)
class FacturaAdmin(admin.ModelAdmin):
# Herncia de Documento
list_display = ('nombre', 'empresa', 'importe', 'categoria', 'subido_por', 'fecha_subida')
list_filter = ('categoria', 'temporada')
search_fields = ('nombre', 'empresa', 'descripcion')
filter_horizontal = ('temporada',)
# Mantenemos la misma automatización
def save_model(self, request, obj, form, change):
if not obj.subido_por:
obj.subido_por = request.user
super().save_model(request, obj, form, change)

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class DocumentosConfig(AppConfig):
name = 'documentos'

View file

@ -1,37 +0,0 @@
# Generated by Django 6.0.2 on 2026-02-17 11:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('temporadas', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Documento',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titulo', models.CharField(max_length=100, verbose_name='Título del documento')),
('archivo', models.FileField(upload_to='ingenieria_docs/')),
('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('suspension', 'Suspensión'), ('motor', 'Motor/Powertrain'), ('electronica', 'Electrónica'), ('general', 'General / Normativa')], max_length=20)),
('tipo', models.CharField(choices=[('diseno', 'Diseño / CAD'), ('simulacion', 'Simulación'), ('informe', 'Informe Técnico'), ('factura', 'Factura / Presupuesto'), ('otro', 'Otro')], default='informe', max_length=20)),
('descripcion', models.TextField(blank=True, null=True, verbose_name='Descripción o notas')),
('fecha_subida', models.DateTimeField(auto_now_add=True)),
('subido_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documentos_subidos', to=settings.AUTH_USER_MODEL)),
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')),
],
options={
'verbose_name': 'Documento Técnico',
'verbose_name_plural': 'Documentos de Ingeniería',
'ordering': ['-fecha_subida'],
},
),
]

View file

@ -1,66 +0,0 @@
# Generated by Django 6.0.2 on 2026-03-06 19:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('documentos', '0001_initial'),
('temporadas', '0002_temporada_miembros'),
]
operations = [
migrations.CreateModel(
name='Factura',
fields=[
('documento_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='documentos.documento')),
('empresa', models.CharField(max_length=100, verbose_name='Nombre de la empresa')),
('importe', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Importe (€)')),
],
options={
'verbose_name': 'Factura',
'verbose_name_plural': 'Facturas',
},
bases=('documentos.documento',),
),
migrations.AlterModelOptions(
name='documento',
options={'ordering': ['-fecha_subida'], 'verbose_name': 'Documento', 'verbose_name_plural': 'Documentos'},
),
migrations.RemoveField(
model_name='documento',
name='titulo',
),
migrations.AddField(
model_name='documento',
name='nombre',
field=models.CharField(default='Documento antiguo', max_length=100, verbose_name='Nombre del documento'),
preserve_default=False,
),
migrations.AlterField(
model_name='documento',
name='categoria',
field=models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('e_powertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('normativa', 'General / Normativa')], max_length=20),
),
migrations.AlterField(
model_name='documento',
name='descripcion',
field=models.TextField(blank=True, null=True, verbose_name='Descripción del documento'),
),
migrations.RemoveField(
model_name='documento',
name='temporada',
),
migrations.AlterField(
model_name='documento',
name='tipo',
field=models.CharField(choices=[('fabricacion', 'Fabricación'), ('diseno', 'Diseño / CAD'), ('concepto', 'Concepto'), ('simulacion', 'Simulación'), ('dossier_patrocinado', 'Dossier Patrocinio'), ('informe', 'Informe'), ('tutorial', 'Tutorial'), ('otro', 'Otro')], default='informe', max_length=20),
),
migrations.AddField(
model_name='documento',
name='temporada',
field=models.ManyToManyField(related_name='documentos_asociados', to='temporadas.temporada'),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 6.0.2 on 2026-06-30 16:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('documentos', '0002_factura_alter_documento_options_and_more'),
]
operations = [
migrations.AddField(
model_name='factura',
name='estado',
field=models.CharField(choices=[('pendiente', 'Pendiente'), ('aceptada', 'Aceptada'), ('rechazada', 'Rechazada')], default='pendiente', max_length=20),
),
]

View file

@ -1,83 +0,0 @@
from django.db import models
from users.models import CustomUser
from temporadas.models import Temporada
import os
class Documento(models.Model):
# Opciones de categorías (puedes añadir más)
CATEGORIAS = (
('aerodinamica', 'Aerodinámica'),
('chasis', 'Chasis'),
('business', 'Business & Operations'),
('e_powertrain', 'E-Powertrain'),
('electronica', 'Electrónica'),
('sdf', 'SDF'),
('motor_transmision', 'Motor & Transmisión'),
('software', 'Software'),
('normativa', 'General / Normativa'),
)
TIPO_DOC = (
('fabricacion', 'Fabricación'),
('diseno', 'Diseño / CAD'),
('concepto', 'Concepto'),
('simulacion', 'Simulación'),
('dossier_patrocinado', 'Dossier Patrocinio'),
('informe', 'Informe'),
('tutorial', 'Tutorial'),
('otro', 'Otro'),
)
# CAMPOS
nombre = models.CharField(max_length=100, verbose_name="Nombre del documento")
# Aquí definimos dónde se guardan los archivos.
# upload_to='ingenieria/' creará esa carpeta automáticamente.
archivo = models.FileField(upload_to='ingenieria_docs/')
categoria = models.CharField(max_length=20, choices=CATEGORIAS)
tipo = models.CharField(max_length=20, choices=TIPO_DOC, default='informe')
descripcion = models.TextField(blank=True, null=True, verbose_name="Descripción del documento")
fecha_subida = models.DateTimeField(auto_now_add=True)
# RELACIONES
# Relacion Documento - Temporadas (Relación 1..* a 1..* )
temporada = models.ManyToManyField(Temporada, related_name="documentos_asociados")
# 2. Relación 1 a N con el Usuario (Miembros)
subido_por = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, related_name="documentos_subidos")
class Meta:
verbose_name = "Documento"
verbose_name_plural = "Documentos"
ordering = ['-fecha_subida']
def __str__(self):
return f"{self.nombre} ({self.get_categoria_display()})"
def delete(self, *args, **kwargs):
# Esto borra el archivo físico del disco duro cuando borras la entrada en la base de datos
if self.archivo:
if os.path.isfile(self.archivo.path):
os.remove(self.archivo.path)
super().delete(*args, **kwargs)
ESTADO_FACTURA = [
('pendiente', 'Pendiente'),
('aceptada', 'Aceptada'),
('rechazada', 'Rechazada'),
]
# Herencia de la clase Factura: Factura hereda de Documento
class Factura(Documento):
# Al heredar de Documento, ya tiene nombre, archivo, categoria, etc.
empresa = models.CharField(max_length=100, verbose_name="Nombre de la empresa")
importe = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Importe (€)")
estado = models.CharField(max_length=20, choices=ESTADO_FACTURA, default='pendiente')
class Meta:
verbose_name = "Factura"
verbose_name_plural = "Facturas"
def __str__(self):
return f"Factura {self.empresa} - {self.importe}"

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View file

@ -1,16 +0,0 @@
"""
ASGI config for gades_manager project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gades_manager.settings')
application = get_asgi_application()

View file

@ -1,141 +0,0 @@
"""
Django settings for gades_manager project.
Generated by 'django-admin startproject' using Django 6.0.2.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-mwlyo^0ax9u9ts5(hw(e&vlgmr$nbla-3az0d%s(lymmp5^)$-'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'users',
'temporadas',
'documentos',
'gestion',
'pruebas',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'gades_manager.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'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',
],
},
},
]
WSGI_APPLICATION = 'gades_manager.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'formula_gades_bd', # El nombre exacto que pusiste en Workbench
'USER': 'root',
'PASSWORD': 'admin1234', # ¡La que apuntaste en el papel!
'HOST': 'localhost', # Significa "este ordenador"
'PORT': '3306',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
BASE_DIR / "static",
]
AUTH_USER_MODEL = 'users.CustomUser'
MEDIA_URL = '/media/' # La URL pública (ej: tudominio.com/media/archivo.pdf)
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # La carpeta física en tu ordenador
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'inicio'
LOGOUT_REDIRECT_URL = 'login'

View file

@ -1,55 +0,0 @@
"""
URL configuration for gades_manager project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.0/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
from django.contrib.auth import views as auth_views
from gestion import views
from users import views as users_views
from temporadas import views as temporadas_views
from pruebas import views as pruebas_views
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.inicio, name='inicio'),
path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(next_page='login'), name='logout'),
path('mi-perfil/', users_views.mi_perfil, name='mi_perfil'),
path('miembros/', users_views.listado_miembros, name='listado_miembros'),
path('gestion/usuarios/', users_views.gestion_usuarios, name='gestion_usuarios'),
path('gestion/usuarios/<int:pk>/editar/', users_views.editar_usuario, name='editar_usuario'),
path('gestion/usuarios/<int:pk>/eliminar/', users_views.eliminar_usuario, name='eliminar_usuario'),
path('gestion/temporadas/', temporadas_views.gestion_temporadas, name='gestion_temporadas'),
path('gestion/temporadas/crear/', temporadas_views.crear_temporada, name='crear_temporada'),
path('gestion/temporadas/<int:pk>/editar/', temporadas_views.editar_temporada, name='editar_temporada'),
path('gestion/temporadas/<int:pk>/eliminar/', temporadas_views.eliminar_temporada, name='eliminar_temporada'),
path('gestion/contabilidad/', views.contabilidad, name='contabilidad'),
path('gestion/contabilidad/gasto/anadir/', views.anadir_gasto, name='anadir_gasto'),
path('gestion/contabilidad/ingreso/anadir/', views.anadir_ingreso, name='anadir_ingreso'),
path('gestion/contabilidad/factura/<int:pk>/aceptar/', views.aceptar_factura, name='aceptar_factura'),
path('gestion/contabilidad/factura/<int:pk>/rechazar/', views.rechazar_factura, name='rechazar_factura'),
path('patrocinios/', views.patrocinios, name='patrocinios'),
path('patrocinios/proponer/', views.proponer_patrocinio, name='proponer_patrocinio'),
path('patrocinios/<int:pk>/editar/', views.editar_patrocinio, name='editar_patrocinio'),
path('patrocinios/<int:pk>/estado/', views.cambiar_estado_patrocinio, name='cambiar_estado_patrocinio'),
path('pruebas/', pruebas_views.listado_pruebas, name='listado_pruebas'),
path('pruebas/nueva/', pruebas_views.crear_prueba, name='crear_prueba'),
path('pruebas/<int:pk>/', pruebas_views.detalle_prueba, name='detalle_prueba'),
path('pruebas/<int:pk>/editar/', pruebas_views.editar_prueba, name='editar_prueba'),
path('pruebas/<int:pk>/eliminar/', pruebas_views.eliminar_prueba, name='eliminar_prueba'),
path('pruebas/<int:pk>/csv/subir/', pruebas_views.subir_csv, name='subir_csv'),
]

View file

@ -1,16 +0,0 @@
"""
WSGI config for gades_manager project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gades_manager.settings')
application = get_wsgi_application()

View file

@ -1,46 +0,0 @@
from django.contrib import admin
from .models import Patrocinio, Pieza, Gasto, Ingreso
# INLINES
class PiezaInline(admin.TabularInline):
model = Pieza
extra = 1
# PANEL PARA ADMINISTRACIÓN
@admin.register(Patrocinio)
class PatrocinioAdmin(admin.ModelAdmin):
list_display = ('empresa', 'estado', 'tipo_patrocinio', 'importe_economico', 'temporada', 'contacto_equipo')
# Para editar el estado y pasar de En contacto a Aceptado o Denagado desde la lista, sin tener que hacerlo manual
list_editable = ('estado',)
list_filter = ('estado', 'tipo_patrocinio', 'temporada')
search_fields = ('empresa', 'persona_contacto', 'email_contacto')
# Metemos las piezas en el patrocinio, por si hiciera falta
inlines = [PiezaInline]
@admin.register(Pieza)
class PiezaAdmin(admin.ModelAdmin):
list_display = ('nombre', 'cantidad', 'patrocinio')
search_fields = ('nombre', 'patrocinio__empresa')
# CONTABILIDAD (Gastos e Ingresos)
@admin.register(Gasto)
class GastoAdmin(admin.ModelAdmin):
# Heredamos los campos de Contabilidad
list_display = ('concepto', 'importe', 'categoria', 'fecha', 'temporada')
list_filter = ('categoria', 'temporada')
search_fields = ('concepto', 'observaciones')
# Barra de navegación por fecha de gasto
date_hierarchy = 'fecha'
@admin.register(Ingreso)
class IngresoAdmin(admin.ModelAdmin):
list_display = ('concepto', 'importe', 'categoria', 'fecha', 'temporada')
list_filter = ('categoria', 'temporada')
search_fields = ('concepto', 'observaciones')
# Barra de navegación por fecha de ingreso
date_hierarchy = 'fecha'

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class GestionConfig(AppConfig):
name = 'gestion'

View file

@ -1,46 +0,0 @@
from django import forms
from .models import Gasto, Ingreso, Patrocinio
class GastoForm(forms.ModelForm):
class Meta:
model = Gasto
fields = ['concepto', 'importe', 'categoria', 'observaciones']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
class IngresoForm(forms.ModelForm):
class Meta:
model = Ingreso
fields = ['concepto', 'importe', 'categoria', 'observaciones']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
class PatrocinioForm(forms.ModelForm):
class Meta:
model = Patrocinio
fields = ['empresa', 'email_contacto', 'persona_contacto', 'tipo_patrocinio']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
class PatrocinioEditForm(forms.ModelForm):
class Meta:
model = Patrocinio
fields = ['empresa', 'email_contacto', 'persona_contacto', 'tipo_patrocinio', 'estado', 'importe_economico']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'

View file

@ -1,45 +0,0 @@
# Generated by Django 6.0.2 on 2026-02-19 10:14
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('temporadas', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Factura',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=100, verbose_name='Concepto de la factura')),
('empresa', models.CharField(max_length=100, verbose_name='Empresa / Proveedor')),
('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('general', 'General')], max_length=30)),
('descripcion', models.TextField(blank=True, null=True, verbose_name='Breve descripción')),
('archivo', models.FileField(upload_to='facturas/', verbose_name='Foto o PDF de la factura')),
('subido_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Usuario que la sube')),
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')),
],
),
migrations.CreateModel(
name='Patrocinador',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('empresa', models.CharField(max_length=100, verbose_name='Nombre de la Empresa')),
('email_contacto', models.EmailField(max_length=254, verbose_name='Correo de contacto')),
('tipo_empresa', models.CharField(max_length=30, verbose_name='Tipo de la Empresa')),
('tipo_patrocinio', models.CharField(choices=[('economico', 'Económico'), ('piezas/materiales', 'Piezas/Materiales'), ('mixto', 'Mixto (Dinero y Piezas)')], max_length=30)),
('detalle_piezas', models.TextField(blank=True, help_text='Detallar qué se ofrece si es en especies', null=True)),
('estado', models.CharField(choices=[('en contacto', 'En contacto'), ('aceptado', 'Aceptado / Activo'), ('denegado', 'Denegado')], default='contacto', max_length=20)),
('contacto_equipo', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Miembro al cargo')),
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')),
],
),
]

View file

@ -1,92 +0,0 @@
# Generated by Django 6.0.2 on 2026-03-06 19:29
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestion', '0001_initial'),
('temporadas', '0002_temporada_miembros'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='patrocinador',
name='contacto_equipo',
),
migrations.RemoveField(
model_name='patrocinador',
name='temporada',
),
migrations.CreateModel(
name='Gasto',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('concepto', models.CharField(max_length=150)),
('importe', models.DecimalField(decimal_places=2, max_digits=10)),
('fecha', models.DateField()),
('observaciones', models.TextField(blank=True, null=True)),
('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('e_powertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('general', 'General')], max_length=30, verbose_name='Área del gasto')),
('doc_justificativo', models.FileField(blank=True, null=True, upload_to='contabilidad/gastos/', verbose_name='Ticket o Factura (PDF/IMG)')),
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='%(class)s_registrados', to='temporadas.temporada')),
],
options={
'verbose_name': 'Gasto',
'verbose_name_plural': 'Gastos',
},
),
migrations.CreateModel(
name='Ingreso',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('concepto', models.CharField(max_length=150)),
('importe', models.DecimalField(decimal_places=2, max_digits=10)),
('fecha', models.DateField()),
('observaciones', models.TextField(blank=True, null=True)),
('categoria', models.CharField(choices=[('patrocinador', 'Patrocinador'), ('donación', 'Donación'), ('premio', 'Premio'), ('recaudación', 'Recaudación'), ('Cuotas', 'Cuotas'), ('Ventas', 'ventas')], max_length=30, verbose_name='Categoria del Ingreso')),
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='%(class)s_registrados', to='temporadas.temporada')),
],
options={
'verbose_name': 'Ingreso',
'verbose_name_plural': 'Ingresos',
},
),
migrations.CreateModel(
name='Patrocinio',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('empresa', models.CharField(max_length=100, verbose_name='Nombre de la Empresa')),
('persona_contacto', models.CharField(blank=True, max_length=100, null=True, verbose_name='Persona de contacto')),
('email_contacto', models.EmailField(max_length=254, verbose_name='Correo de contacto')),
('tipo_patrocinio', models.CharField(choices=[('economico', 'Económico'), ('piezas/materiales', 'Piezas/Materiales'), ('mixto', 'Mixto (Dinero y Piezas)')], max_length=30)),
('estado', models.CharField(choices=[('en_contacto', 'En contacto'), ('aceptado', 'Aceptado / Activo'), ('denegado', 'Denegado')], default='en_contacto', max_length=20)),
('importe_economico', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='Importe (€)')),
('fecha_contacto', models.DateField(auto_now_add=True)),
('contacto_equipo', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patrocinios_gestionados', to=settings.AUTH_USER_MODEL, verbose_name='Persona al cargo del Patrocinio')),
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='patrocinios', to='temporadas.temporada', verbose_name='Temporada del patrocinio')),
],
options={
'verbose_name': 'Patrocinio',
'verbose_name_plural': 'Patrocinios',
},
),
migrations.CreateModel(
name='Pieza',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=100, verbose_name='Nombre de la pieza/material')),
('cantidad', models.PositiveIntegerField(default=1)),
('patrocinio', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='piezas', to='gestion.patrocinio')),
],
),
migrations.DeleteModel(
name='Factura',
),
migrations.DeleteModel(
name='Patrocinador',
),
]

View file

@ -1,106 +0,0 @@
from django.db import models
from django.conf import settings
from temporadas.models import Temporada
class Patrocinio(models.Model):
TIPO_PATROCINIO = (
('economico', 'Económico'),
('piezas/materiales', 'Piezas/Materiales'),
('mixto', 'Mixto (Dinero y Piezas)'),
)
ESTADOS = (
('en_contacto', 'En contacto'),
('aceptado', 'Aceptado / Activo'),
('denegado', 'Denegado'),
)
empresa = models.CharField(max_length=100, verbose_name="Nombre de la Empresa")
persona_contacto = models.CharField(max_length=100, blank=True, null=True, verbose_name="Persona de contacto")
email_contacto = models.EmailField(verbose_name="Correo de contacto")
tipo_patrocinio = models.CharField(max_length=30, choices=TIPO_PATROCINIO)
estado = models.CharField(max_length=20, choices=ESTADOS, default='en_contacto')
# Si el patrocinio es únicamente material, se quedará en 0. Si es económico o mixto, se rellena
importe_economico = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name="Importe (€)")
fecha_contacto = models.DateField(auto_now_add=True)
# Relaciones
temporada = models.ForeignKey(Temporada, on_delete=models.CASCADE, related_name="patrocinios", verbose_name="Temporada del patrocinio")
contacto_equipo = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="patrocinios_gestionados", verbose_name="Persona al cargo del Patrocinio")
class Meta:
verbose_name = "Patrocinio"
verbose_name_plural = "Patrocinios"
def __str__(self):
return f"{self.empresa} ({self.get_estado_display()})"
class Pieza(models.Model):
nombre = models.CharField(max_length=100, verbose_name="Nombre de la pieza/material")
cantidad = models.PositiveIntegerField(default=1)
# COMPOSICIÓN ESTRICTA: Si se borra el Patrocinio, se borran las Piezas irremediablemente (CASCADE)
patrocinio = models.ForeignKey(Patrocinio, on_delete=models.CASCADE, related_name="piezas")
def __str__(self):
return f"{self.cantidad}x {self.nombre} (De: {self.patrocinio.empresa})"
class Contabilidad(models.Model):
concepto = models.CharField(max_length=150)
importe = models.DecimalField(max_digits=10, decimal_places=2)
fecha = models.DateField()
observaciones = models.TextField(blank=True, null=True)
# Relación con Temporada. PROTECT evita que borres una temporada si tiene contabilidad asociada (por temas legales)
temporada = models.ForeignKey(Temporada, on_delete=models.PROTECT, related_name="%(class)s_registrados")
class Meta:
abstract = True #marcamos la clase como abstracta
ordering = ['-fecha']
class Gasto(Contabilidad):
CATEGORIAS_GASTOS = (
('aerodinamica', 'Aerodinámica'),
('chasis', 'Chasis'),
('business', 'Business & Operations'),
('e_powertrain', 'E-Powertrain'),
('electronica', 'Electrónica'),
('sdf', 'SDF'),
('motor_transmision', 'Motor & Transmisión'),
('software', 'Software'),
('general', 'General'),
)
# Hereda concepto, importe, fecha, observaciones y temporada automáticamente
categoria = models.CharField(max_length=30, choices=CATEGORIAS_GASTOS, verbose_name="Área del gasto")
doc_justificativo = models.FileField(upload_to='contabilidad/gastos/', blank=True, null=True, verbose_name="Ticket o Factura (PDF/IMG)")
class Meta:
verbose_name = "Gasto"
verbose_name_plural = "Gastos"
def __str__(self):
return f"[-{self.importe}€] {self.concepto} ({self.temporada})"
class Ingreso(Contabilidad):
CATEGORIAS_INGRESO = (
('patrocinador', 'Patrocinador'),
('donación', 'Donación'),
('premio', 'Premio'),
('recaudación', 'Recaudación'),
('Cuotas', 'Cuotas'),
('Ventas', 'ventas'),
)
# Hereda concepto, importe, fecha, observaciones y temporada automáticamente
categoria = models.CharField(max_length=30, choices=CATEGORIAS_INGRESO, verbose_name="Categoria del Ingreso")
class Meta:
verbose_name = "Ingreso"
verbose_name_plural = "Ingresos"
def __str__(self):
return f"[+{self.importe}€] {self.concepto} ({self.temporada})"

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,202 +0,0 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.db.models import Sum
from datetime import date
from temporadas.models import Temporada
from documentos.models import Factura
from users.decorators import require_rol
from .models import Gasto, Ingreso, Patrocinio
from .forms import GastoForm, IngresoForm, PatrocinioForm, PatrocinioEditForm
# Categorías válidas en Gasto (Documento usa 'normativa' en su lugar de 'general')
_CATEGORIAS_GASTO_VALIDAS = {c[0] for c in Gasto.CATEGORIAS_GASTOS}
@login_required
def inicio(request):
temporada_activa = Temporada.objects.filter(actual=True).first()
return render(request, 'index.html', {'temporada_actual': temporada_activa})
@require_rol('directiva')
def contabilidad(request):
temporada_actual = Temporada.objects.filter(actual=True).first()
gastos = []
ingresos = []
facturas_pendientes = []
total_gastos = 0
total_ingresos = 0
presupuesto_inicial = 0
presupuesto_actual = 0
if temporada_actual:
gastos = Gasto.objects.filter(temporada=temporada_actual)
ingresos = Ingreso.objects.filter(temporada=temporada_actual)
facturas_pendientes = Factura.objects.filter(
temporada=temporada_actual, estado='pendiente'
)
total_gastos = gastos.aggregate(total=Sum('importe'))['total'] or 0
total_ingresos = ingresos.aggregate(total=Sum('importe'))['total'] or 0
presupuesto_inicial = temporada_actual.presupuesto
presupuesto_actual = presupuesto_inicial - total_gastos + total_ingresos
return render(request, 'contabilidad.html', {
'temporada_actual': temporada_actual,
'gastos': gastos,
'ingresos': ingresos,
'facturas_pendientes': facturas_pendientes,
'total_gastos': total_gastos,
'total_ingresos': total_ingresos,
'presupuesto_inicial': presupuesto_inicial,
'presupuesto_actual': presupuesto_actual,
'gasto_form': GastoForm(),
'ingreso_form': IngresoForm(),
})
@require_rol('directiva')
@require_POST
def anadir_gasto(request):
temporada_actual = Temporada.objects.filter(actual=True).first()
if not temporada_actual:
messages.error(request, 'No hay temporada activa.')
return redirect('contabilidad')
form = GastoForm(request.POST)
if form.is_valid():
gasto = form.save(commit=False)
gasto.fecha = date.today()
gasto.temporada = temporada_actual
gasto.save()
messages.success(request, 'Gasto añadido correctamente.')
else:
messages.error(request, 'Error al añadir el gasto. Revisa los datos.')
return redirect('contabilidad')
@require_rol('directiva')
@require_POST
def anadir_ingreso(request):
temporada_actual = Temporada.objects.filter(actual=True).first()
if not temporada_actual:
messages.error(request, 'No hay temporada activa.')
return redirect('contabilidad')
form = IngresoForm(request.POST)
if form.is_valid():
ingreso = form.save(commit=False)
ingreso.fecha = date.today()
ingreso.temporada = temporada_actual
ingreso.save()
messages.success(request, 'Ingreso añadido correctamente.')
else:
messages.error(request, 'Error al añadir el ingreso. Revisa los datos.')
return redirect('contabilidad')
@require_rol('directiva')
@require_POST
def aceptar_factura(request, pk):
factura = get_object_or_404(Factura, pk=pk)
temporada_actual = Temporada.objects.filter(actual=True).first()
if not temporada_actual:
messages.error(request, 'No hay temporada activa.')
return redirect('contabilidad')
categoria = factura.categoria if factura.categoria in _CATEGORIAS_GASTO_VALIDAS else 'general'
Gasto.objects.create(
concepto=factura.nombre,
importe=factura.importe,
fecha=date.today(),
categoria=categoria,
temporada=temporada_actual,
observaciones=f'Factura de {factura.empresa}',
doc_justificativo=factura.archivo,
)
factura.estado = 'aceptada'
factura.save()
messages.success(request, f'Factura de {factura.empresa} aceptada y registrada como gasto.')
return redirect('contabilidad')
@require_rol('directiva')
@require_POST
def rechazar_factura(request, pk):
factura = get_object_or_404(Factura, pk=pk)
factura.estado = 'rechazada'
factura.save()
messages.success(request, f'Factura de {factura.empresa} rechazada.')
return redirect('contabilidad')
@login_required
def patrocinios(request):
temporada_actual = Temporada.objects.filter(actual=True).first()
pendientes = []
aceptados = []
denegados = []
if temporada_actual:
qs = Patrocinio.objects.filter(temporada=temporada_actual).select_related('contacto_equipo')
pendientes = qs.filter(estado='en_contacto')
aceptados = qs.filter(estado='aceptado')
denegados = qs.filter(estado='denegado')
return render(request, 'patrocinios.html', {
'temporada_actual': temporada_actual,
'pendientes': pendientes,
'aceptados': aceptados,
'denegados': denegados,
'form': PatrocinioForm(),
})
@login_required
@require_POST
def proponer_patrocinio(request):
temporada_actual = Temporada.objects.filter(actual=True).first()
if not temporada_actual:
messages.error(request, 'No hay temporada activa. No se puede proponer un patrocinio.')
return redirect('patrocinios')
form = PatrocinioForm(request.POST)
if form.is_valid():
empresa = form.cleaned_data['empresa'].strip()
if Patrocinio.objects.filter(empresa__iexact=empresa, temporada=temporada_actual).exists():
messages.error(request, f'Ya existe un patrocinio con "{empresa}" en la temporada actual.')
else:
patrocinio = form.save(commit=False)
patrocinio.estado = 'en_contacto'
patrocinio.temporada = temporada_actual
patrocinio.contacto_equipo = request.user
patrocinio.save()
messages.success(request, f'Patrocinio de "{empresa}" propuesto correctamente.')
else:
messages.error(request, 'Error en el formulario. Revisa los datos.')
return redirect('patrocinios')
@require_rol('directiva')
def editar_patrocinio(request, pk):
patrocinio = get_object_or_404(Patrocinio, pk=pk)
form = PatrocinioEditForm(request.POST or None, instance=patrocinio)
if request.method == 'POST' and form.is_valid():
form.save()
messages.success(request, 'Patrocinio actualizado correctamente.')
return redirect('patrocinios')
return render(request, 'editar_patrocinio.html', {'form': form, 'patrocinio': patrocinio})
@require_rol('directiva')
@require_POST
def cambiar_estado_patrocinio(request, pk):
patrocinio = get_object_or_404(Patrocinio, pk=pk)
nuevo_estado = request.POST.get('estado')
if nuevo_estado in ('aceptado', 'denegado', 'en_contacto'):
patrocinio.estado = nuevo_estado
patrocinio.save()
messages.success(request, f'Estado de "{patrocinio.empresa}" actualizado.')
return redirect('patrocinios')

View file

@ -1,22 +0,0 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gades_manager.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View file

@ -1,50 +0,0 @@
from django.contrib import admin
from .models import Prueba, Telemetria, Variable
# INLINES
# Usamos inlines que nos sirve para cuando se este creando un registro de Telemetría, podremos añadir sus Variables en la misma pantalla sin tener que ir a otro menú
class VariableInline(admin.TabularInline):
model = Variable
extra = 1
class TelemetriaInline(admin.TabularInline):
model = Telemetria
extra = 0
# PANELES DE ADMINISTRACIÓN
@admin.register(Prueba)
class PruebaAdmin(admin.ModelAdmin):
list_display = ('nombre', 'categoria', 'fecha_inicio', 'temporada', 'realizado_por')
# Filtros para encontrar los test dado una temporada y una área técnica
list_filter = ('categoria', 'temporada')
# Buscador de texto
search_fields = ('nombre', 'descripcion', 'resultados')
# Una barra de navegación en la parte superior, para poder hacer clic en un año y en un mes y mostrar todos los datos de ese me
date_hierarchy = 'fecha_inicio'
# Mostramos los archivos de telemetría directamente dentro del test
inlines = [TelemetriaInline]
@admin.register(Telemetria)
class TelemetriaAdmin(admin.ModelAdmin):
list_display = ('nombre', 'prueba', 'fecha_subida')
# Filtra test de telemetría basandose en la temporada o área tecnica de su test correspondiente
list_filter = ('prueba__temporada', 'prueba__categoria')
# Buscador del nombre de la telemetría o por el nombre de la prueba
search_fields = ('nombre', 'prueba__nombre')
# Mostramos las variables de su telemetría
inlines = [VariableInline]
@admin.register(Variable)
class VariableAdmin(admin.ModelAdmin):
# Aunque se pueden crear desde el Inline, dejamos su tabla propia por si fuese necesario, por prevenir
list_display = ('nombre', 'unidad_medida', 'telemetria')
search_fields = ('nombre', 'telemetria__nombre')

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class PruebasConfig(AppConfig):
name = 'pruebas'

View file

@ -1,22 +0,0 @@
from django import forms
from .models import Prueba, Telemetria
class PruebaForm(forms.ModelForm):
class Meta:
model = Prueba
fields = ['nombre', 'descripcion', 'fecha_inicio', 'fecha_fin', 'categoria', 'resultados', 'temporada']
widgets = {
'fecha_inicio': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'fecha_fin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
}
class TelemetriaForm(forms.ModelForm):
class Meta:
model = Telemetria
fields = ['nombre', 'archivo_csv']
widgets = {
'nombre': forms.TextInput(attrs={'class': 'form-control form-control-sm'}),
'archivo_csv': forms.ClearableFileInput(attrs={'class': 'form-control form-control-sm'}),
}

View file

@ -1,34 +0,0 @@
# Generated by Django 6.0.2 on 2026-02-19 10:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('temporadas', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='TestGeneral',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=150, verbose_name='Nombre del Test')),
('descripcion', models.TextField(verbose_name='Objetivo / Descripción del test')),
('fecha_inicio', models.DateField()),
('fecha_fin', models.DateField()),
('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('general', 'General')], default='general', max_length=30)),
('resultados', models.TextField(blank=True, null=True, verbose_name='Conclusiones y Resultados')),
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')),
],
options={
'verbose_name': 'Test General',
'verbose_name_plural': 'Tests Generales',
'ordering': ['-fecha_inicio'],
},
),
]

View file

@ -1,67 +0,0 @@
# Generated by Django 6.0.2 on 2026-03-06 19:29
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pruebas', '0001_initial'),
('temporadas', '0002_temporada_miembros'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Prueba',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=150, verbose_name='Nombre del Test')),
('descripcion', models.TextField(verbose_name='Objetivo / Descripción de la prueba')),
('fecha_inicio', models.DateField()),
('fecha_fin', models.DateField()),
('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('general', 'General')], default='general', max_length=30)),
('resultados', models.TextField(blank=True, null=True, verbose_name='Conclusiones y Resultados')),
('realizado_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pruebas_realizadas', to=settings.AUTH_USER_MODEL, verbose_name='Realizado por')),
('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pruebas', to='temporadas.temporada')),
],
options={
'verbose_name': 'Prueba',
'verbose_name_plural': 'Pruebas',
'ordering': ['-fecha_inicio'],
},
),
migrations.CreateModel(
name='Telemetria',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=100, verbose_name='Nombre del registro')),
('archivo_csv', models.FileField(upload_to='telemetria/archivos_csv/', verbose_name='Archivo de Datos (CSV)')),
('fecha_subida', models.DateTimeField(auto_now_add=True)),
('prueba', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='archivos_telemetria', to='pruebas.prueba')),
],
options={
'verbose_name': 'Registro de Telemetría',
'verbose_name_plural': 'Registros de Telemetría',
},
),
migrations.CreateModel(
name='Variable',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=50, verbose_name='Nombre ')),
('unidad_medida', models.CharField(blank=True, max_length=20, null=True, verbose_name='Unidad de medida')),
('descripcion', models.TextField(blank=True, null=True)),
('telemetria', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variables', to='pruebas.telemetria')),
],
options={
'verbose_name': 'Variable de Telemetría',
'verbose_name_plural': 'Variables de Telemetría',
},
),
migrations.DeleteModel(
name='TestGeneral',
),
]

View file

@ -1,78 +0,0 @@
from django.db import models
from django.conf import settings
from temporadas.models import Temporada
import os
class Prueba(models.Model):
CATEGORIAS_TEST = (
('aerodinamica', 'Aerodinámica'),
('chasis', 'Chasis'),
('epowertrain', 'E-Powertrain'),
('electronica', 'Electrónica'),
('sdf', 'SDF'),
('motor_transmision', 'Motor & Transmisión'),
('software', 'Software'),
('general', 'General'),
)
nombre = models.CharField(max_length=150, verbose_name="Nombre del Test")
descripcion = models.TextField(verbose_name="Objetivo / Descripción de la prueba")
fecha_inicio = models.DateField()
fecha_fin = models.DateField()
categoria = models.CharField(max_length=30, choices=CATEGORIAS_TEST, default='general')
resultados = models.TextField(blank=True, null=True, verbose_name="Conclusiones y Resultados")
# Relaciones
# Relacion con Temporada
temporada = models.ForeignKey(Temporada, on_delete=models.CASCADE, related_name="pruebas")
#Relacion con Usuario
realizado_por = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="pruebas_realizadas", verbose_name="Realizado por")
class Meta:
verbose_name = "Prueba"
verbose_name_plural = "Pruebas"
ordering = ['-fecha_inicio']
def __str__(self):
return f"{self.nombre} ({self.fecha_inicio})"
class Telemetria(models.Model):
nombre = models.CharField(max_length=100, verbose_name="Nombre del registro")
archivo_csv = models.FileField(upload_to='telemetria/archivos_csv/', verbose_name="Archivo de Datos (CSV)")
fecha_subida = models.DateTimeField(auto_now_add=True)
# La telemetría se obtiene durante una Prueba en pista
prueba = models.ForeignKey(Prueba, on_delete=models.CASCADE, related_name="archivos_telemetria")
class Meta:
verbose_name = "Registro de Telemetría"
verbose_name_plural = "Registros de Telemetría"
def __str__(self):
return f"Telemetría: {self.nombre} (De: {self.prueba.nombre})"
def delete(self, *args, **kwargs):
# Borra el archivo físico del disco duro cuando borras la entrada en la base de datos
if self.archivo_csv:
if os.path.isfile(self.archivo_csv.path):
os.remove(self.archivo_csv.path)
super().delete(*args, **kwargs)
class Variable(models.Model):
nombre = models.CharField(max_length=50, verbose_name="Nombre ")
unidad_medida = models.CharField(max_length=20, verbose_name="Unidad de medida", blank=True, null=True)
descripcion = models.TextField(blank=True, null=True)
# COMPOSICIÓN
telemetria = models.ForeignKey(Telemetria, on_delete=models.CASCADE, related_name="variables")
class Meta:
verbose_name = "Variable de Telemetría"
verbose_name_plural = "Variables de Telemetría"
def __str__(self):
unidad = f" [{self.unidad_medida}]" if self.unidad_medida else ""
return f"{self.nombre}{unidad} (De: {self.telemetria.nombre})"

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,102 +0,0 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from users.decorators import require_rol
from temporadas.models import Temporada
from .models import Prueba
from .forms import PruebaForm, TelemetriaForm
def puede_subir_csv(user):
return user.rol == 'directiva' or user.especialidad == 'software'
@login_required
def listado_pruebas(request):
temporada_actual = Temporada.objects.filter(actual=True).first()
temporada_id = request.GET.get('temporada') or (temporada_actual.pk if temporada_actual else None)
categoria = request.GET.get('categoria')
pruebas = Prueba.objects.all()
if temporada_id:
pruebas = pruebas.filter(temporada_id=temporada_id)
if categoria:
pruebas = pruebas.filter(categoria=categoria)
return render(request, 'listado_pruebas.html', {
'pruebas': pruebas,
'temporadas': Temporada.objects.all(),
'categorias': Prueba.CATEGORIAS_TEST,
'temporada_seleccionada': str(temporada_id) if temporada_id else '',
'categoria_seleccionada': categoria or '',
})
@login_required
def detalle_prueba(request, pk):
prueba = get_object_or_404(Prueba, pk=pk)
puede_editar = request.user.rol == 'directiva' or (
request.user.rol == 'jefe_area' and prueba.realizado_por_id == request.user.id
)
return render(request, 'detalle_prueba.html', {
'prueba': prueba,
'puede_editar': puede_editar,
'puede_subir_csv': puede_subir_csv(request.user),
'form_csv': TelemetriaForm(),
})
@require_rol('directiva', 'jefe_area')
def crear_prueba(request):
form = PruebaForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
prueba = form.save(commit=False)
prueba.realizado_por = request.user
prueba.save()
messages.success(request, 'Test creado correctamente.')
return redirect('listado_pruebas')
return render(request, 'editar_prueba.html', {'form': form, 'prueba': None})
@require_rol('directiva', 'jefe_area')
def editar_prueba(request, pk):
prueba = get_object_or_404(Prueba, pk=pk)
if request.user.rol == 'jefe_area' and prueba.realizado_por_id != request.user.id:
messages.error(request, 'Solo puedes editar los tests que tú has creado.')
return redirect('detalle_prueba', pk=prueba.pk)
form = PruebaForm(request.POST or None, instance=prueba)
if request.method == 'POST' and form.is_valid():
form.save()
messages.success(request, 'Test actualizado correctamente.')
return redirect('detalle_prueba', pk=prueba.pk)
return render(request, 'editar_prueba.html', {'form': form, 'prueba': prueba})
@require_rol('directiva')
@require_POST
def eliminar_prueba(request, pk):
prueba = get_object_or_404(Prueba, pk=pk)
prueba.delete()
messages.success(request, f'Test "{prueba.nombre}" eliminado.')
return redirect('listado_pruebas')
@login_required
@require_POST
def subir_csv(request, pk):
prueba = get_object_or_404(Prueba, pk=pk)
if not puede_subir_csv(request.user):
messages.error(request, 'No tienes permiso para subir archivos de telemetría.')
return redirect('detalle_prueba', pk=prueba.pk)
form = TelemetriaForm(request.POST, request.FILES)
if form.is_valid():
telemetria = form.save(commit=False)
telemetria.prueba = prueba
telemetria.save()
messages.success(request, 'Archivo de telemetría subido correctamente.')
else:
messages.error(request, 'No se pudo subir el archivo. Revisa el formulario.')
return redirect('detalle_prueba', pk=prueba.pk)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 MiB

View file

@ -1,111 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Intranet | Formula Gades{% endblock title %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/estilos.css' %}">
{% block extra_head %}{% endblock extra_head %}
<style>
html, body { overflow: hidden; height: 100%; }
</style>
</head>
<body class="bg-light text-dark d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-xl navbar-dark bg-dark sticky-top shadow-sm">
<div class="container-fluid px-4">
<a class="navbar-brand d-flex align-items-center" href="{% url 'inicio' %}">
<img src="{% static 'images/logo_gades.png' %}" alt="Logo Gades" height="40" class="me-2">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto align-items-center">
<li class="nav-item dropdown px-2">
<a class="nav-link dropdown-toggle" href="#" id="areasDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Áreas Técnicas
</a>
<ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="areasDropdown">
<li><a class="dropdown-menu-item dropdown-item" href="#">Aerodinámica</a></li>
<li><a class="dropdown-item" href="#">Chasis</a></li>
<li><a class="dropdown-item" href="#">Bussiness & Operation</a></li>
<li><a class="dropdown-item" href="#">E_Powertrain</a></li>
<li><a class="dropdown-item" href="#">Electrónica</a></li>
<li><a class="dropdown-item" href="#">SDF</a></li>
<li><a class="dropdown-item" href="#">Motor & Transmisión</a></li>
<li><a class="dropdown-item" href="#">Software</a></li>
</ul>
</li>
<li class="nav-item px-2">
<a class="nav-link" href="{% url 'listado_pruebas' %}">Pruebas</a>
</li>
<li class="nav-item px-2">
<a class="nav-link" href="{% url 'patrocinios' %}">Patrocinios</a>
</li>
<li class="nav-item px-2">
<a class="nav-link" href="{% url 'listado_miembros' %}">Miembros</a>
</li>
{% if user.is_authenticated and user.rol == 'directiva' %}
<li class="nav-item px-2">
<a class="nav-link text-warning" href="{% url 'contabilidad' %}">Contabilidad</a>
</li>
<li class="nav-item dropdown px-2">
<a class="nav-link dropdown-toggle text-warning" href="#" id="gestionDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Gestión
</a>
<ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="gestionDropdown">
<li><a class="dropdown-item" href="{% url 'gestion_usuarios' %}">Gestión de Usuarios</a></li>
<li><a class="dropdown-item" href="{% url 'gestion_temporadas' %}">Gestión de Temporadas</a></li>
</ul>
</li>
{% endif %}
<li class="nav-item dropdown ms-3 border-start ps-3">
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<div class="bg-secondary rounded-circle d-flex align-items-center justify-content-center text-white fw-bold shadow-sm" style="width: 35px; height: 35px;">
{{ user.first_name|slice:":1"|upper }}
</div>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><span class="dropdown-item-text fw-bold text-muted">Hola, {{ user.first_name }}</span></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'mi_perfil' %}">Mi Perfil</a></li>
<li>
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit" class="dropdown-item text-danger">Cerrar Sesión</button>
</form>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<main class="container-fluid py-3 px-4 flex-grow-1 d-flex flex-column" style="min-height: 0;">
{% block content %}
{% endblock content %}
</main>
<footer class="bg-dark text-white text-center py-2 mt-auto border-top border-secondary">
<p class="mb-0 small">&copy; 2026 Formula Gades - Universidad de Cádiz. Todos los derechos reservados.</p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock extra_js %}
</body>
</html>

View file

@ -1,247 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Contabilidad | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #ffc107;">
<h1 class="display-6 fw-bold mb-1">Contabilidad</h1>
<p class="text-white-50 mb-0">
{% if temporada_actual %}{{ temporada_actual.nombre }}{% else %}Sin temporada activa{% endif %}
</p>
</div>
</div>
</div>
{% if messages %}
<div class="row mb-2 flex-shrink-0">
<div class="col-12">
{% for message in messages %}
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %} py-2 small mb-1">{{ message }}</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if not temporada_actual %}
<div class="row">
<div class="col-12">
<div class="alert alert-warning">No hay ninguna temporada activa. Activa una temporada desde <a href="{% url 'gestion_temporadas' %}">Gestión de Temporadas</a>.</div>
</div>
</div>
{% else %}
{# ── Bloque 1: 4 tarjetas resumen ── #}
<div class="row g-3 mb-3 flex-shrink-0">
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm text-center h-100" style="border-top: 4px solid #0d6efd !important;">
<div class="card-body py-3">
<p class="text-secondary small mb-1">Presupuesto Inicial</p>
<h4 class="fw-bold text-primary mb-0">{{ presupuesto_inicial|floatformat:2 }} €</h4>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm text-center h-100" style="border-top: 4px solid #198754 !important;">
<div class="card-body py-3">
<p class="text-secondary small mb-1">Total Ingresos</p>
<h4 class="fw-bold text-success mb-0">+{{ total_ingresos|floatformat:2 }} €</h4>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm text-center h-100" style="border-top: 4px solid #dc3545 !important;">
<div class="card-body py-3">
<p class="text-secondary small mb-1">Total Gastos</p>
<h4 class="fw-bold text-danger mb-0">-{{ total_gastos|floatformat:2 }} €</h4>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm text-center h-100"
style="border-top: 4px solid {% if presupuesto_actual >= 0 %}#198754{% else %}#dc3545{% endif %} !important;">
<div class="card-body py-3">
<p class="text-secondary small mb-1">Presupuesto Actual</p>
<h4 class="fw-bold {% if presupuesto_actual >= 0 %}text-success{% else %}text-danger{% endif %} mb-0">
{{ presupuesto_actual|floatformat:2 }} €
</h4>
</div>
</div>
</div>
</div>
{# ── Bloque 2: Gastos | Ingresos ── #}
<div class="row g-3 mb-3" style="overflow-y: auto; max-height: 340px;">
{# Columna Gastos #}
<div class="col-12 col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-bold mb-0 text-danger">Gastos</h6>
<button class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#modalGasto">+ Añadir</button>
</div>
<div style="max-height: 220px; overflow-y: auto;">
{% if gastos %}
<ul class="list-group list-group-flush">
{% for gasto in gastos %}
<li class="list-group-item px-0 py-2 d-flex justify-content-between align-items-start">
<div>
<div class="fw-semibold small">{{ gasto.concepto }}</div>
<div class="text-secondary" style="font-size:0.75rem;">{{ gasto.get_categoria_display }} · {{ gasto.fecha|date:"d/m/Y" }}</div>
</div>
<span class="badge bg-danger rounded-pill ms-2">-{{ gasto.importe|floatformat:2 }} €</span>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-secondary small text-center py-3 mb-0">Sin gastos registrados.</p>
{% endif %}
</div>
</div>
</div>
</div>
{# Columna Ingresos #}
<div class="col-12 col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-bold mb-0 text-success">Ingresos</h6>
<button class="btn btn-sm btn-outline-success" data-bs-toggle="modal" data-bs-target="#modalIngreso">+ Añadir</button>
</div>
<div style="max-height: 220px; overflow-y: auto;">
{% if ingresos %}
<ul class="list-group list-group-flush">
{% for ingreso in ingresos %}
<li class="list-group-item px-0 py-2 d-flex justify-content-between align-items-start">
<div>
<div class="fw-semibold small">{{ ingreso.concepto }}</div>
<div class="text-secondary" style="font-size:0.75rem;">{{ ingreso.get_categoria_display }} · {{ ingreso.fecha|date:"d/m/Y" }}</div>
</div>
<span class="badge bg-success rounded-pill ms-2">+{{ ingreso.importe|floatformat:2 }} €</span>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-secondary small text-center py-3 mb-0">Sin ingresos registrados.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{# ── Bloque 3: Facturas pendientes ── #}
<div class="row flex-shrink-0">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-3">
<h6 class="fw-bold mb-3">Facturas Pendientes</h6>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-dark">
<tr>
<th>Nombre</th>
<th>Empresa</th>
<th>Importe</th>
<th>Categoría</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for factura in facturas_pendientes %}
<tr>
<td>{{ factura.nombre }}</td>
<td>{{ factura.empresa }}</td>
<td class="fw-semibold">{{ factura.importe|floatformat:2 }} €</td>
<td>{{ factura.get_categoria_display }}</td>
<td class="d-flex gap-2">
<form method="post" action="{% url 'aceptar_factura' factura.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-success fw-bold">Aceptar</button>
</form>
<form method="post" action="{% url 'rechazar_factura' factura.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-danger">Rechazar</button>
</form>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center text-secondary py-3">No hay facturas pendientes.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}{# end if temporada_actual #}
{# ── Modal Añadir Gasto ── #}
<div class="modal fade" id="modalGasto" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Añadir Gasto</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="{% url 'anadir_gasto' %}">
{% csrf_token %}
<div class="modal-body">
{% for field in gasto_form %}
<div class="mb-3">
<label class="form-label fw-semibold">{{ field.label }}</label>
{{ field }}
{% for error in field.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-danger fw-bold">Guardar Gasto</button>
</div>
</form>
</div>
</div>
</div>
{# ── Modal Añadir Ingreso ── #}
<div class="modal fade" id="modalIngreso" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Añadir Ingreso</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="{% url 'anadir_ingreso' %}">
{% csrf_token %}
<div class="modal-body">
{% for field in ingreso_form %}
<div class="mb-3">
<label class="form-label fw-semibold">{{ field.label }}</label>
{{ field }}
{% for error in field.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-success fw-bold">Guardar Ingreso</button>
</div>
</form>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,112 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ prueba.nombre }} | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm d-flex justify-content-between align-items-center" style="border-left: 5px solid #0d6efd;">
<div>
<h1 class="display-6 fw-bold mb-1">{{ prueba.nombre }}</h1>
<p class="text-white-50 mb-0">{{ prueba.get_categoria_display }} · Temporada {{ prueba.temporada.nombre }}</p>
</div>
<div class="d-flex gap-2">
{% if puede_editar %}
<a href="{% url 'editar_prueba' prueba.pk %}" class="btn btn-outline-light fw-bold">Editar</a>
{% endif %}
{% if user.rol == 'directiva' %}
<button type="button" class="btn btn-outline-danger fw-bold" data-bs-toggle="modal" data-bs-target="#eliminarModal">Eliminar</button>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row g-4" style="overflow-y: auto;">
{% if messages %}
<div class="col-12">
{% for message in messages %}
<div class="alert alert-success py-2 small mb-0">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<div class="col-12 col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<h5 class="fw-semibold">Objetivo / Descripción</h5>
<p>{{ prueba.descripcion }}</p>
<div class="row mb-3">
<div class="col-6"><strong>Fecha inicio:</strong> {{ prueba.fecha_inicio|date:"d/m/Y" }}</div>
<div class="col-6"><strong>Fecha fin:</strong> {{ prueba.fecha_fin|date:"d/m/Y" }}</div>
</div>
<p><strong>Realizado por:</strong> {{ prueba.realizado_por.first_name|default:"—" }}</p>
{% if prueba.resultados %}
<h5 class="fw-semibold mt-4">Conclusiones y Resultados</h5>
<p>{{ prueba.resultados }}</p>
{% endif %}
</div>
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<h5 class="fw-semibold mb-3">Archivos de telemetría</h5>
<ul class="list-group list-group-flush mb-3">
{% for telemetria in prueba.archivos_telemetria.all %}
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<a href="{{ telemetria.archivo_csv.url }}">{{ telemetria.nombre }}</a>
<span class="text-secondary small">{{ telemetria.fecha_subida|date:"d/m/Y" }}</span>
</li>
{% empty %}
<li class="list-group-item px-0 text-secondary small">No hay archivos subidos todavía.</li>
{% endfor %}
</ul>
{% if puede_subir_csv %}
<form method="post" action="{% url 'subir_csv' prueba.pk %}" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-2">
<label class="form-label small fw-semibold">Nombre</label>
{{ form_csv.nombre }}
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Archivo CSV</label>
{{ form_csv.archivo_csv }}
</div>
<button type="submit" class="btn btn-sm btn-primary fw-bold">Subir CSV</button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
{% if user.rol == 'directiva' %}
<div class="modal fade" id="eliminarModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmar eliminación</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
¿Seguro que quieres eliminar el test <strong>{{ prueba.nombre }}</strong>? Esta acción no se puede deshacer.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form method="post" action="{% url 'eliminar_prueba' prueba.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger fw-bold">Eliminar</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock content %}

View file

@ -1,44 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Editar Patrocinio | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
<h1 class="display-6 fw-bold mb-1">Editar Patrocinio</h1>
<p class="text-white-50 mb-0">{{ patrocinio.empresa }}</p>
</div>
</div>
</div>
<div class="row g-4" style="overflow-y: auto;">
<div class="col-12 col-lg-6">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<h5 class="card-title fw-bold mb-3">Datos del Patrocinio</h5>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label fw-semibold">{{ field.label }}</label>
{{ field }}
{% for error in field.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-primary fw-bold px-4">Guardar Cambios</button>
<a href="{% url 'patrocinios' %}" class="btn btn-outline-secondary px-4">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,102 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{% if prueba %}Editar Test{% else %}Nuevo Test{% endif %} | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
<h1 class="display-6 fw-bold mb-1">{% if prueba %}Editar Test{% else %}Nuevo Test{% endif %}</h1>
<p class="text-white-50 mb-0">{% if prueba %}{{ prueba.nombre }}{% else %}Rellena los datos del nuevo test.{% endif %}</p>
</div>
</div>
</div>
<div class="row g-4" style="overflow-y: auto;">
<div class="col-12 col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.nombre.id_for_label }}" class="form-label fw-semibold">Nombre del test</label>
<input type="text" name="nombre" id="{{ form.nombre.id_for_label }}"
class="form-control {% if form.nombre.errors %}is-invalid{% endif %}"
value="{{ form.nombre.value|default:'' }}" placeholder="Ej: Test de frenada">
{% for error in form.nombre.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ form.descripcion.id_for_label }}" class="form-label fw-semibold">Objetivo / Descripción</label>
<textarea name="descripcion" id="{{ form.descripcion.id_for_label }}" rows="3"
class="form-control {% if form.descripcion.errors %}is-invalid{% endif %}">{{ form.descripcion.value|default:'' }}</textarea>
{% for error in form.descripcion.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label for="{{ form.fecha_inicio.id_for_label }}" class="form-label fw-semibold">Fecha de inicio</label>
{{ form.fecha_inicio }}
{% for error in form.fecha_inicio.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
<div class="col-6">
<label for="{{ form.fecha_fin.id_for_label }}" class="form-label fw-semibold">Fecha de fin</label>
{{ form.fecha_fin }}
{% for error in form.fecha_fin.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label for="{{ form.categoria.id_for_label }}" class="form-label fw-semibold">Categoría</label>
<select name="categoria" id="{{ form.categoria.id_for_label }}" class="form-select">
{% for valor, etiqueta in form.fields.categoria.choices %}
<option value="{{ valor }}" {% if form.categoria.value == valor %}selected{% endif %}>{{ etiqueta }}</option>
{% endfor %}
</select>
</div>
<div class="col-6">
<label for="{{ form.temporada.id_for_label }}" class="form-label fw-semibold">Temporada</label>
<select name="temporada" id="{{ form.temporada.id_for_label }}"
class="form-select {% if form.temporada.errors %}is-invalid{% endif %}">
{% for temporada in form.fields.temporada.queryset %}
<option value="{{ temporada.pk }}" {% if form.temporada.value|stringformat:"s" == temporada.pk|stringformat:"s" %}selected{% endif %}>{{ temporada.nombre }}</option>
{% endfor %}
</select>
{% for error in form.temporada.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="mb-4">
<label for="{{ form.resultados.id_for_label }}" class="form-label fw-semibold">Conclusiones y Resultados</label>
<textarea name="resultados" id="{{ form.resultados.id_for_label }}" rows="3"
class="form-control {% if form.resultados.errors %}is-invalid{% endif %}">{{ form.resultados.value|default:'' }}</textarea>
{% for error in form.resultados.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary fw-bold px-4">Guardar</button>
<a href="{% if prueba %}{% url 'detalle_prueba' prueba.pk %}{% else %}{% url 'listado_pruebas' %}{% endif %}" class="btn btn-outline-secondary px-4">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,96 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{% if temporada %}Editar Temporada{% else %}Nueva Temporada{% endif %} | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
<h1 class="display-6 fw-bold mb-1">{% if temporada %}Editar Temporada{% else %}Nueva Temporada{% endif %}</h1>
<p class="text-white-50 mb-0">{% if temporada %}{{ temporada.nombre }}{% else %}Rellena los datos de la nueva temporada.{% endif %}</p>
</div>
</div>
</div>
<div class="row g-4" style="overflow-y: auto;">
<div class="col-12 col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.nombre.id_for_label }}" class="form-label fw-semibold">Nombre</label>
<input type="text" name="nombre" id="{{ form.nombre.id_for_label }}"
class="form-control {% if form.nombre.errors %}is-invalid{% endif %}"
value="{{ form.nombre.value|default:'' }}" placeholder="Ej: Gades 2025-26">
{% for error in form.nombre.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label for="{{ form.fecha_inicio.id_for_label }}" class="form-label fw-semibold">Fecha de inicio</label>
{{ form.fecha_inicio }}
{% for error in form.fecha_inicio.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
<div class="col-6">
<label for="{{ form.fecha_fin.id_for_label }}" class="form-label fw-semibold">Fecha de fin</label>
{{ form.fecha_fin }}
{% for error in form.fecha_fin.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="mb-3">
<label for="{{ form.presupuesto.id_for_label }}" class="form-label fw-semibold">Presupuesto (€)</label>
<input type="number" name="presupuesto" id="{{ form.presupuesto.id_for_label }}"
class="form-control {% if form.presupuesto.errors %}is-invalid{% endif %}"
value="{{ form.presupuesto.value|default:'0.00' }}" step="0.01" min="0">
{% for error in form.presupuesto.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-4 form-check">
<input type="checkbox" name="actual" id="{{ form.actual.id_for_label }}"
class="form-check-input" {% if form.actual.value %}checked{% endif %}>
<label for="{{ form.actual.id_for_label }}" class="form-check-label fw-semibold">
Marcar como temporada activa
</label>
<div class="form-text">Solo puede haber una temporada activa. Al marcar esta se desactivarán las demás automáticamente.</div>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">Miembros</label>
<div class="border rounded p-3" style="max-height: 220px; overflow-y: auto;">
{% for checkbox in form.miembros %}
<div class="form-check">
{{ checkbox.tag }}
<label class="form-check-label" for="{{ checkbox.id_for_label }}">
{{ checkbox.choice_label }}
</label>
</div>
{% empty %}
<span class="text-secondary small">No hay usuarios en el sistema.</span>
{% endfor %}
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary fw-bold px-4">Guardar</button>
<a href="{% url 'gestion_temporadas' %}" class="btn btn-outline-secondary px-4">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,70 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Editar Usuario | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
<h1 class="display-6 fw-bold mb-1">Editar Usuario</h1>
<p class="text-white-50 mb-0">{{ usuario.username }}</p>
</div>
</div>
</div>
<div class="row g-4" style="overflow-y: auto;">
<div class="col-12 col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<h5 class="card-title fw-bold mb-3">Datos del Usuario</h5>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.username.id_for_label }}" class="form-label fw-semibold">{{ form.username.label }}</label>
{{ form.username }}
{% for error in form.username.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
<div class="row g-2">
<div class="col-6 mb-3">
<label for="{{ form.first_name.id_for_label }}" class="form-label fw-semibold">{{ form.first_name.label }}</label>
{{ form.first_name }}
</div>
<div class="col-6 mb-3">
<label for="{{ form.last_name.id_for_label }}" class="form-label fw-semibold">{{ form.last_name.label }}</label>
{{ form.last_name }}
</div>
</div>
<div class="mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label fw-semibold">{{ form.email.label }}</label>
{{ form.email }}
{% for error in form.email.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
<div class="row g-2">
<div class="col-6 mb-3">
<label for="{{ form.rol.id_for_label }}" class="form-label fw-semibold">{{ form.rol.label }}</label>
{{ form.rol }}
</div>
<div class="col-6 mb-3">
<label for="{{ form.especialidad.id_for_label }}" class="form-label fw-semibold">{{ form.especialidad.label }}</label>
{{ form.especialidad }}
</div>
</div>
<button type="submit" class="btn btn-primary fw-bold px-4">Guardar Cambios</button>
<a href="{% url 'gestion_usuarios' %}" class="btn btn-outline-secondary px-4">Cancelar</a>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,100 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Gestión de Temporadas | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm d-flex justify-content-between align-items-center" style="border-left: 5px solid #0d6efd;">
<div>
<h1 class="display-6 fw-bold mb-1">Gestión de Temporadas</h1>
<p class="text-white-50 mb-0">Crea, edita y elimina temporadas del equipo.</p>
</div>
<a href="{% url 'crear_temporada' %}" class="btn btn-primary fw-bold">+ Nueva Temporada</a>
</div>
</div>
</div>
<div class="row g-4" style="overflow-y: auto;">
{% if messages %}
<div class="col-12">
{% for message in messages %}
<div class="alert alert-success py-2 small mb-0">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-dark">
<tr>
<th>Nombre</th>
<th>Fecha inicio</th>
<th>Fecha fin</th>
<th>Presupuesto</th>
<th>Miembros</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for temporada in temporadas %}
<tr>
<td class="fw-semibold">{{ temporada.nombre }}</td>
<td>{{ temporada.fecha_inicio|date:"d/m/Y" }}</td>
<td>{{ temporada.fecha_fin|date:"d/m/Y" }}</td>
<td>{{ temporada.presupuesto|floatformat:2 }} €</td>
<td>{{ temporada.miembros.count }}</td>
<td>
{% if temporada.actual %}
<span class="badge bg-success">Activa</span>
{% else %}
<span class="badge bg-secondary">Anterior</span>
{% endif %}
</td>
<td class="d-flex gap-2">
<a href="{% url 'editar_temporada' temporada.pk %}" class="btn btn-sm btn-outline-primary">Editar</a>
<button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#eliminarModal{{ temporada.pk }}">Eliminar</button>
</td>
</tr>
<div class="modal fade" id="eliminarModal{{ temporada.pk }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmar eliminación</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
¿Seguro que quieres eliminar la temporada <strong>{{ temporada.nombre }}</strong>? Esta acción no se puede deshacer.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form method="post" action="{% url 'eliminar_temporada' temporada.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger fw-bold">Eliminar</button>
</form>
</div>
</div>
</div>
</div>
{% empty %}
<tr>
<td colspan="7" class="text-center text-secondary py-4">No hay temporadas creadas todavía.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,125 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Gestión de Usuarios | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
<h1 class="display-6 fw-bold mb-1">Gestión de Usuarios</h1>
<p class="text-white-50 mb-0">Administra las cuentas del sistema: edita o elimina usuarios.</p>
</div>
</div>
</div>
<div class="row g-4" style="overflow-y: auto;">
{% if messages %}
<div class="col-12">
{% for message in messages %}
<div class="alert alert-success py-2 small mb-0">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="get" class="row g-2 align-items-end mb-4">
<div class="col-12 col-md-3">
<label class="form-label small text-secondary mb-1">Rol</label>
<select name="rol" class="form-select">
<option value="">Todos los roles</option>
{% for value, label in rol_choices %}
<option value="{{ value }}" {% if filtro_rol == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-12 col-md-3">
<label class="form-label small text-secondary mb-1">Área Técnica</label>
<select name="especialidad" class="form-select">
<option value="">Todas las áreas</option>
{% for value, label in especialidad_choices %}
<option value="{{ value }}" {% if filtro_especialidad == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-12 col-md-4">
<label class="form-label small text-secondary mb-1">Apellido</label>
<input type="text" name="apellido" class="form-control" placeholder="Buscar por apellido..." value="{{ filtro_apellido }}">
</div>
<div class="col-12 col-md-2 d-flex gap-2">
<button type="submit" class="btn btn-primary fw-bold flex-grow-1">Buscar</button>
{% if filtro_rol or filtro_especialidad or filtro_apellido %}
<a href="{% url 'gestion_usuarios' %}" class="btn btn-outline-secondary" title="Limpiar filtros">&times;</a>
{% endif %}
</div>
</form>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-dark">
<tr>
<th>Usuario</th>
<th>Nombre</th>
<th>Apellidos</th>
<th>Email</th>
<th>Área Técnica</th>
<th>Rol</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for usuario in usuarios %}
<tr>
<td>{{ usuario.username }}</td>
<td>{{ usuario.first_name }}</td>
<td>{{ usuario.last_name }}</td>
<td>{{ usuario.email }}</td>
<td>{{ usuario.get_especialidad_display|default:"—" }}</td>
<td>{{ usuario.get_rol_display }}</td>
<td class="d-flex gap-2">
<a href="{% url 'editar_usuario' usuario.pk %}" class="btn btn-sm btn-outline-primary">Editar</a>
<button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#eliminarModal{{ usuario.pk }}">Eliminar</button>
</td>
</tr>
<div class="modal fade" id="eliminarModal{{ usuario.pk }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmar eliminación</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
¿Seguro que quieres eliminar al usuario <strong>{{ usuario.username }}</strong>? Esta acción no se puede deshacer.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<form method="post" action="{% url 'eliminar_usuario' usuario.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger fw-bold">Eliminar</button>
</form>
</div>
</div>
</div>
</div>
{% empty %}
<tr>
<td colspan="7" class="text-center text-secondary py-4">No se han encontrado usuarios con los filtros seleccionados.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,115 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Inicio | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm d-flex justify-content-between align-items-center flex-wrap" style="border-left: 5px solid #0d6efd;">
<div>
<h1 class="display-6 fw-bold mb-1">Bienvenido, {{ user.first_name }} {{ user.last_name }}</h1>
<p class="text-white-50 mb-0">Panel de control de la plataforma de gestión interna.</p>
</div>
<div class="text-end mt-2 mt-md-0">
<span class="badge bg-primary fs-6 px-3 py-2 shadow-sm">
Temporada Activa: {% if temporada_actual %}{{ temporada_actual.nombre }}{% else %}Ninguna activa{% endif %}
</span>
</div>
</div>
</div>
</div>
<div class="row g-4 flex-grow-1" style="min-height: 0;">
<div class="col-12 col-lg-8 d-flex flex-column" style="min-height: 0;">
<div class="shadow rounded-3 overflow-hidden flex-grow-1" style="min-height: 0;">
<img src="{% static 'images/monoplaza_gades.jpg' %}" alt="Monoplaza Formula Gades"
style="width: 100%; height: 100%; object-fit: cover; display: block;">
</div>
</div>
<div class="col-12 col-lg-4">
<div class="row g-3">
{% if user.rol != 'directiva' %}
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-3">
<h5 class="card-title fw-bold text-dark mb-1">Mi Área Técnica</h5>
<p class="text-secondary small mb-2">Acceso a documentación, planos y buzón de compras de tu departamento.</p>
<a href="#" class="btn btn-outline-primary btn-sm fw-bold px-3">Acceder a mi Área</a>
</div>
</div>
</div>
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-3">
<h5 class="card-title fw-bold text-dark mb-1">Pruebas y Telemetría</h5>
<p class="text-secondary small mb-2">Carga de archivos de registros CSV y procesamiento analítico de telemetría.</p>
<a href="#" class="btn btn-outline-primary btn-sm fw-bold px-3">Ver Telemetría</a>
</div>
</div>
</div>
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-3">
<h5 class="card-title fw-bold text-dark mb-1">Miembros del Equipo</h5>
<p class="text-secondary small mb-2">Directorio completo de integrantes, correos corporativos y roles.</p>
<a href="{% url 'listado_miembros' %}" class="btn btn-outline-primary btn-sm fw-bold px-3">Ver Directorio</a>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-3">
<h5 class="card-title fw-bold text-dark mb-1">Pruebas y Telemetría</h5>
<p class="text-secondary small mb-2">Carga de archivos de registros CSV y procesamiento analítico de telemetría.</p>
<a href="#" class="btn btn-outline-primary btn-sm fw-bold px-3">Ver Telemetría</a>
</div>
</div>
</div>
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-3">
<h5 class="card-title fw-bold text-dark mb-1">Miembros del Equipo</h5>
<p class="text-secondary small mb-2">Directorio completo de integrantes, correos corporativos y roles.</p>
<a href="{% url 'listado_miembros' %}" class="btn btn-outline-primary btn-sm fw-bold px-3">Ver Directorio</a>
</div>
</div>
</div>
<div class="col-12">
<div class="card border-0 shadow-sm" style="border-left: 4px solid #ffc107;">
<div class="card-body p-3">
<h5 class="card-title fw-bold text-dark mb-1">Contabilidad Global</h5>
<p class="text-secondary small mb-2">Supervisión del presupuesto anual y control del buzón financiero de facturas.</p>
<a href="#" class="btn btn-warning btn-sm fw-bold text-dark px-3">Gestionar Finanzas</a>
</div>
</div>
</div>
<div class="col-12">
<div class="card border-0 shadow-sm" style="border-left: 4px solid #ffc107;">
<div class="card-body p-3">
<h5 class="card-title fw-bold text-dark mb-1">Administración del Sistema</h5>
<p class="text-secondary small mb-2">Módulo avanzado de configuración de permisos, usuarios y temporadas.</p>
<a href="#" class="btn btn-warning btn-sm fw-bold text-dark px-3">Panel de Control</a>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,90 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Miembros | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
<h1 class="display-6 fw-bold mb-1">Miembros del Equipo</h1>
<p class="text-white-50 mb-0">Directorio completo de integrantes de Formula Gades.</p>
</div>
</div>
</div>
<div class="row g-4" style="overflow-y: auto;">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="get" class="row g-2 align-items-end mb-4">
<div class="col-12 col-md-3">
<label class="form-label small text-secondary mb-1">Rol</label>
<select name="rol" class="form-select">
<option value="">Todos los roles</option>
{% for value, label in rol_choices %}
<option value="{{ value }}" {% if filtro_rol == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-12 col-md-3">
<label class="form-label small text-secondary mb-1">Área Técnica</label>
<select name="especialidad" class="form-select">
<option value="">Todas las áreas</option>
{% for value, label in especialidad_choices %}
<option value="{{ value }}" {% if filtro_especialidad == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-12 col-md-4">
<label class="form-label small text-secondary mb-1">Apellido</label>
<input type="text" name="apellido" class="form-control" placeholder="Buscar por apellido..." value="{{ filtro_apellido }}">
</div>
<div class="col-12 col-md-2 d-flex gap-2">
<button type="submit" class="btn btn-primary fw-bold flex-grow-1">Buscar</button>
{% if filtro_rol or filtro_especialidad or filtro_apellido %}
<a href="{% url 'listado_miembros' %}" class="btn btn-outline-secondary" title="Limpiar filtros">&times;</a>
{% endif %}
</div>
</form>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-dark">
<tr>
<th>Usuario</th>
<th>Nombre</th>
<th>Apellidos</th>
<th>Email</th>
<th>Área Técnica</th>
<th>Rol</th>
</tr>
</thead>
<tbody>
{% for miembro in miembros %}
<tr>
<td>{{ miembro.username }}</td>
<td>{{ miembro.first_name }}</td>
<td>{{ miembro.last_name }}</td>
<td>{{ miembro.email }}</td>
<td>{{ miembro.get_especialidad_display|default:"—" }}</td>
<td>{{ miembro.get_rol_display }}</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center text-secondary py-4">No se han encontrado miembros con los filtros seleccionados.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,91 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Pruebas | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm d-flex justify-content-between align-items-center" style="border-left: 5px solid #0d6efd;">
<div>
<h1 class="display-6 fw-bold mb-1">Pruebas</h1>
<p class="text-white-50 mb-0">Tests realizados durante la temporada.</p>
</div>
{% if user.rol == 'directiva' or user.rol == 'jefe_area' %}
<a href="{% url 'crear_prueba' %}" class="btn btn-primary fw-bold">+ Nuevo Test</a>
{% endif %}
</div>
</div>
</div>
<div class="row g-4" style="overflow-y: auto;">
{% if messages %}
<div class="col-12">
{% for message in messages %}
<div class="alert alert-success py-2 small mb-0">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="get" class="row g-2 align-items-end mb-4">
<div class="col-auto">
<label class="form-label fw-semibold small mb-1">Temporada</label>
<select name="temporada" class="form-select form-select-sm">
{% for temporada in temporadas %}
<option value="{{ temporada.pk }}" {% if temporada_seleccionada == temporada.pk|stringformat:"s" %}selected{% endif %}>{{ temporada.nombre }}</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<label class="form-label fw-semibold small mb-1">Categoría</label>
<select name="categoria" class="form-select form-select-sm">
<option value="">Todas</option>
{% for valor, etiqueta in categorias %}
<option value="{{ valor }}" {% if categoria_seleccionada == valor %}selected{% endif %}>{{ etiqueta }}</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-outline-primary">Buscar</button>
</div>
</form>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-dark">
<tr>
<th>Nombre</th>
<th>Fecha inicio</th>
<th>Fecha fin</th>
<th>Categoría</th>
<th>Realizado por</th>
</tr>
</thead>
<tbody>
{% for prueba in pruebas %}
<tr role="button" onclick="window.location='{% url 'detalle_prueba' prueba.pk %}'">
<td class="fw-semibold">{{ prueba.nombre }}</td>
<td>{{ prueba.fecha_inicio|date:"d/m/Y" }}</td>
<td>{{ prueba.fecha_fin|date:"d/m/Y" }}</td>
<td>{{ prueba.get_categoria_display }}</td>
<td>{{ prueba.realizado_por.first_name|default:"—" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center text-secondary py-4">No hay tests todavía.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,97 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Iniciar Sesión | Formula Gades</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
html, body {
height: 100%;
overflow: hidden;
}
body {
background-color: #111;
}
.login-bg {
position: fixed;
inset: 0;
background-image: url("{% static 'images/monoplaza_gades.jpg' %}");
background-size: cover;
background-position: center;
filter: brightness(0.35);
z-index: 0;
}
.login-card {
position: relative;
z-index: 1;
background: rgba(20, 20, 20, 0.88);
border: 1px solid rgba(255,255,255,0.08);
backdrop-filter: blur(6px);
}
.form-control {
background-color: #1e1e1e;
border-color: #444;
color: #f0f0f0;
}
.form-control:focus {
background-color: #1e1e1e;
border-color: #0d6efd;
color: #f0f0f0;
box-shadow: 0 0 0 0.2rem rgba(13,110,253,.25);
}
.form-control::placeholder { color: #888; }
.form-label { color: #ccc; }
</style>
</head>
<body class="d-flex align-items-center justify-content-center min-vh-100">
<div class="login-bg"></div>
<div class="login-card rounded-4 shadow-lg p-4 p-md-5" style="width: 100%; max-width: 420px;">
<div class="text-center mb-4">
<img src="{% static 'images/logo_gades.png' %}" alt="Formula Gades" height="60" class="mb-3">
<h4 class="text-white fw-bold mb-1">Plataforma de gestión interna</h4>
<p class="text-secondary small">Bienvenido al sistema interno del equipo.</p>
</div>
{% if form.errors %}
<div class="alert alert-danger py-2 small" role="alert">
Usuario o contraseña incorrectos. Inténtalo de nuevo.
</div>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
<div class="mb-3">
<label for="id_username" class="form-label fw-semibold">Usuario</label>
<input type="text" name="username" id="id_username"
class="form-control"
placeholder="Introduce tu usuario"
autofocus required>
</div>
<div class="mb-4">
<label for="id_password" class="form-label fw-semibold">Contraseña</label>
<input type="password" name="password" id="id_password"
class="form-control"
placeholder="Introduce tu contraseña"
required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary fw-bold py-2">
Iniciar Sesión
</button>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View file

@ -1,121 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Mi Perfil | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm" style="border-left: 5px solid #0d6efd;">
<h1 class="display-6 fw-bold mb-1">Mi Perfil</h1>
<p class="text-white-50 mb-0">Consulta y edita tus datos personales.</p>
</div>
</div>
</div>
<div class="row g-4" style="overflow-y: auto;">
{% if messages %}
<div class="col-12">
{% for message in messages %}
<div class="alert alert-success py-2 small mb-0">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<div class="col-12 col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<h5 class="card-title fw-bold mb-3">Datos Personales</h5>
<div class="row g-2 mb-3">
<div class="col-6">
<label class="form-label small text-secondary mb-1">Rol en el equipo</label>
<input type="text" class="form-control" value="{{ user.get_rol_display }}" disabled>
</div>
<div class="col-6">
<label class="form-label small text-secondary mb-1">Área técnica</label>
<input type="text" class="form-control" value="{{ user.get_especialidad_display|default:'—' }}" disabled>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ perfil_form.username.id_for_label }}" class="form-label fw-semibold">{{ perfil_form.username.label }}</label>
{{ perfil_form.username }}
{% for error in perfil_form.username.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
<div class="row g-2">
<div class="col-6 mb-3">
<label for="{{ perfil_form.first_name.id_for_label }}" class="form-label fw-semibold">{{ perfil_form.first_name.label }}</label>
{{ perfil_form.first_name }}
</div>
<div class="col-6 mb-3">
<label for="{{ perfil_form.last_name.id_for_label }}" class="form-label fw-semibold">{{ perfil_form.last_name.label }}</label>
{{ perfil_form.last_name }}
</div>
</div>
<div class="mb-3">
<label for="{{ perfil_form.email.id_for_label }}" class="form-label fw-semibold">{{ perfil_form.email.label }}</label>
{{ perfil_form.email }}
{% for error in perfil_form.email.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
<button type="submit" name="guardar_perfil" class="btn btn-primary fw-bold px-4">Guardar Cambios</button>
</form>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<h5 class="card-title fw-bold mb-3">Cambiar Contraseña</h5>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ password_form.old_password.id_for_label }}" class="form-label fw-semibold">Contraseña actual</label>
<input type="password" name="{{ password_form.old_password.html_name }}" id="{{ password_form.old_password.id_for_label }}" class="form-control">
{% for error in password_form.old_password.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ password_form.new_password1.id_for_label }}" class="form-label fw-semibold">Nueva contraseña</label>
<input type="password" name="{{ password_form.new_password1.html_name }}" id="{{ password_form.new_password1.id_for_label }}" class="form-control">
{% for error in password_form.new_password1.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ password_form.new_password2.id_for_label }}" class="form-label fw-semibold">Confirmar nueva contraseña</label>
<input type="password" name="{{ password_form.new_password2.html_name }}" id="{{ password_form.new_password2.id_for_label }}" class="form-control">
{% for error in password_form.new_password2.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
{% if password_form.non_field_errors %}
<div class="text-danger small mb-3">{{ password_form.non_field_errors }}</div>
{% endif %}
<button type="submit" name="cambiar_password" class="btn btn-outline-danger fw-bold px-4">Cambiar Contraseña</button>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,269 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Patrocinios | Gades Manager{% endblock title %}
{% block content %}
<div class="row mb-3 flex-shrink-0">
<div class="col-12">
<div class="bg-dark text-white p-3 rounded-3 shadow-sm d-flex justify-content-between align-items-center" style="border-left: 5px solid #0d6efd;">
<div>
<h1 class="display-6 fw-bold mb-1">Patrocinios</h1>
<p class="text-white-50 mb-0">Dossiers y gestión de patrocinadores del equipo.</p>
</div>
<button class="btn btn-primary fw-bold" data-bs-toggle="modal" data-bs-target="#modalProponer">+ Proponer Patrocinio</button>
</div>
</div>
</div>
{% if messages %}
<div class="row mb-2 flex-shrink-0">
<div class="col-12">
{% for message in messages %}
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %} py-2 small mb-1">{{ message }}</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="row g-4" style="overflow-y: auto;">
{# ── Bloque 1: Dossiers PDF ── #}
<div class="col-12 col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4 d-flex flex-column align-items-center text-center">
<div class="mb-3" style="font-size: 3rem; color: #dc3545;">
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" fill="currentColor" viewBox="0 0 16 16">
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
<path d="M4.603 14.087a.81.81 0 0 1-.438-.42c-.195-.388-.13-.776.08-1.102.198-.307.526-.568.897-.787a7.68 7.68 0 0 1 1.482-.645 19.697 19.697 0 0 0 1.062-2.227 7.269 7.269 0 0 1-.43-1.295c-.086-.4-.119-.796-.046-1.136.075-.354.274-.672.65-.823.192-.077.4-.12.602-.077a.7.7 0 0 1 .477.365c.088.164.12.356.127.538.007.188-.012.396-.047.614-.084.51-.27 1.134-.52 1.794a10.954 10.954 0 0 0 .98 1.686 5.753 5.753 0 0 1 1.334.05c.364.066.734.195.96.465.12.144.193.32.2.518.007.192-.047.382-.138.563a1.04 1.04 0 0 1-.354.416.856.856 0 0 1-.51.138c-.331-.014-.654-.196-.933-.417a5.712 5.712 0 0 1-.911-.95 11.651 11.651 0 0 0-1.997.406 11.307 11.307 0 0 1-1.02 1.51c-.292.35-.609.656-.927.787a.793.793 0 0 1-.58.029z"/>
</svg>
</div>
<h5 class="fw-bold mb-1">Dossier de Patrocinio</h5>
<p class="text-secondary small mb-3">Versión en español · Temporada 2025/2026</p>
<div class="d-flex gap-2">
<a href="{% static 'dossiers/Dossier_2025_2026.pdf' %}" target="_blank" class="btn btn-outline-primary fw-bold">Abrir</a>
<a href="{% static 'dossiers/Dossier_2025_2026.pdf' %}" download class="btn btn-primary fw-bold">Descargar</a>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4 d-flex flex-column align-items-center text-center">
<div class="mb-3" style="font-size: 3rem; color: #dc3545;">
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" fill="currentColor" viewBox="0 0 16 16">
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
<path d="M4.603 14.087a.81.81 0 0 1-.438-.42c-.195-.388-.13-.776.08-1.102.198-.307.526-.568.897-.787a7.68 7.68 0 0 1 1.482-.645 19.697 19.697 0 0 0 1.062-2.227 7.269 7.269 0 0 1-.43-1.295c-.086-.4-.119-.796-.046-1.136.075-.354.274-.672.65-.823.192-.077.4-.12.602-.077a.7.7 0 0 1 .477.365c.088.164.12.356.127.538.007.188-.012.396-.047.614-.084.51-.27 1.134-.52 1.794a10.954 10.954 0 0 0 .98 1.686 5.753 5.753 0 0 1 1.334.05c.364.066.734.195.96.465.12.144.193.32.2.518.007.192-.047.382-.138.563a1.04 1.04 0 0 1-.354.416.856.856 0 0 1-.51.138c-.331-.014-.654-.196-.933-.417a5.712 5.712 0 0 1-.911-.95 11.651 11.651 0 0 0-1.997.406 11.307 11.307 0 0 1-1.02 1.51c-.292.35-.609.656-.927.787a.793.793 0 0 1-.58.029z"/>
</svg>
</div>
<h5 class="fw-bold mb-1">Sponsorship Dossier</h5>
<p class="text-secondary small mb-3">English version · Season 2025/2026</p>
<div class="d-flex gap-2">
<a href="{% static 'dossiers/DossierEN_2025_2026.pdf' %}" target="_blank" class="btn btn-outline-primary fw-bold">Open</a>
<a href="{% static 'dossiers/DossierEN_2025_2026.pdf' %}" download class="btn btn-primary fw-bold">Download</a>
</div>
</div>
</div>
</div>
{# ── Bloque 2: Patrocinios agrupados ── #}
<div class="col-12">
{% if not temporada_actual %}
<div class="alert alert-warning">No hay temporada activa. Activa una desde <a href="{% url 'gestion_temporadas' %}">Gestión de Temporadas</a>.</div>
{% else %}
{# Pendientes #}
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom d-flex align-items-center gap-2 py-3">
<span class="badge bg-warning text-dark fs-6">{{ pendientes.count }}</span>
<h6 class="fw-bold mb-0">Pendientes / En contacto</h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-dark">
<tr>
<th>Empresa</th>
<th>Tipo</th>
<th>Persona contacto</th>
<th>Email</th>
<th>Propuesto por</th>
<th>Fecha</th>
{% if user.rol == 'directiva' %}<th>Acciones</th>{% endif %}
</tr>
</thead>
<tbody>
{% for p in pendientes %}
<tr>
<td class="fw-semibold">{{ p.empresa }}</td>
<td>{{ p.get_tipo_patrocinio_display }}</td>
<td>{{ p.persona_contacto|default:"—" }}</td>
<td>{{ p.email_contacto }}</td>
<td>{{ p.contacto_equipo.get_full_name|default:p.contacto_equipo }}</td>
<td>{{ p.fecha_contacto|date:"d/m/Y" }}</td>
{% if user.rol == 'directiva' %}
<td>
<div class="d-flex gap-1">
<form method="post" action="{% url 'cambiar_estado_patrocinio' p.pk %}">
{% csrf_token %}
<input type="hidden" name="estado" value="aceptado">
<button type="submit" class="btn btn-sm btn-success fw-bold">Aceptar</button>
</form>
<form method="post" action="{% url 'cambiar_estado_patrocinio' p.pk %}">
{% csrf_token %}
<input type="hidden" name="estado" value="denegado">
<button type="submit" class="btn btn-sm btn-outline-danger">Rechazar</button>
</form>
<a href="{% url 'editar_patrocinio' p.pk %}" class="btn btn-sm btn-outline-secondary">Editar</a>
</div>
</td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="{% if user.rol == 'directiva' %}7{% else %}6{% endif %}" class="text-center text-secondary py-3">No hay patrocinios pendientes.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# Aceptados #}
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-bottom d-flex align-items-center gap-2 py-3">
<span class="badge bg-success fs-6">{{ aceptados.count }}</span>
<h6 class="fw-bold mb-0">Aceptados</h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-dark">
<tr>
<th>Empresa</th>
<th>Tipo</th>
<th>Importe</th>
<th>Persona contacto</th>
<th>Email</th>
<th>Fecha</th>
{% if user.rol == 'directiva' %}<th>Acciones</th>{% endif %}
</tr>
</thead>
<tbody>
{% for p in aceptados %}
<tr>
<td class="fw-semibold">{{ p.empresa }}</td>
<td>{{ p.get_tipo_patrocinio_display }}</td>
<td>{{ p.importe_economico|floatformat:2 }} €</td>
<td>{{ p.persona_contacto|default:"—" }}</td>
<td>{{ p.email_contacto }}</td>
<td>{{ p.fecha_contacto|date:"d/m/Y" }}</td>
{% if user.rol == 'directiva' %}
<td>
<div class="d-flex gap-1">
<form method="post" action="{% url 'cambiar_estado_patrocinio' p.pk %}">
{% csrf_token %}
<input type="hidden" name="estado" value="en_contacto">
<button type="submit" class="btn btn-sm btn-outline-warning">Revertir</button>
</form>
<a href="{% url 'editar_patrocinio' p.pk %}" class="btn btn-sm btn-outline-secondary">Editar</a>
</div>
</td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="{% if user.rol == 'directiva' %}7{% else %}6{% endif %}" class="text-center text-secondary py-3">No hay patrocinios aceptados.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# Rechazados — colapsado por defecto #}
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom d-flex align-items-center gap-2 py-3" style="cursor:pointer;" data-bs-toggle="collapse" data-bs-target="#collapseRechazados">
<span class="badge bg-danger fs-6">{{ denegados.count }}</span>
<h6 class="fw-bold mb-0 text-secondary">Rechazados</h6>
<span class="ms-auto text-secondary small"></span>
</div>
<div class="collapse" id="collapseRechazados">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-dark">
<tr>
<th>Empresa</th>
<th>Tipo</th>
<th>Persona contacto</th>
<th>Email</th>
<th>Fecha</th>
{% if user.rol == 'directiva' %}<th>Acciones</th>{% endif %}
</tr>
</thead>
<tbody>
{% for p in denegados %}
<tr class="text-secondary">
<td class="fw-semibold">{{ p.empresa }}</td>
<td>{{ p.get_tipo_patrocinio_display }}</td>
<td>{{ p.persona_contacto|default:"—" }}</td>
<td>{{ p.email_contacto }}</td>
<td>{{ p.fecha_contacto|date:"d/m/Y" }}</td>
{% if user.rol == 'directiva' %}
<td>
<div class="d-flex gap-1">
<form method="post" action="{% url 'cambiar_estado_patrocinio' p.pk %}">
{% csrf_token %}
<input type="hidden" name="estado" value="en_contacto">
<button type="submit" class="btn btn-sm btn-outline-warning">Reabrir</button>
</form>
<a href="{% url 'editar_patrocinio' p.pk %}" class="btn btn-sm btn-outline-secondary">Editar</a>
</div>
</td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="{% if user.rol == 'directiva' %}6{% else %}5{% endif %}" class="text-center text-secondary py-3">No hay patrocinios rechazados.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}{# end if temporada_actual #}
</div>
</div>
{# ── Modal Proponer Patrocinio ── #}
<div class="modal fade" id="modalProponer" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Proponer Patrocinio</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="{% url 'proponer_patrocinio' %}">
{% csrf_token %}
<div class="modal-body">
{% for field in form %}
<div class="mb-3">
<label class="form-label fw-semibold">{{ field.label }}</label>
{{ field }}
{% for error in field.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<p class="text-secondary small mb-0">El estado quedará como <strong>Pendiente</strong> hasta que la directiva lo revise. Tu nombre quedará registrado como proponente.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary fw-bold">Proponer</button>
</div>
</form>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,19 +0,0 @@
from django.contrib import admin
from .models import Temporada
@admin.register(Temporada)
class TemporadaAdmin(admin.ModelAdmin):
# Columnas principales.
list_display = ('nombre', 'fecha_inicio', 'fecha_fin', 'presupuesto', 'actual')
# Permite marcar o desmarcar el check de "actual" directamente desde la tabla, sin entrar al detalle
list_editable = ('actual',)
# Filtro para filtrar por temporadas históricas o actual
list_filter = ('actual',)
# Buscador de temporadas por nombre
search_fields = ('nombre',)
# Filtro para ver los miembros de una temporada
filter_horizontal = ('miembros',)

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class TemporadasConfig(AppConfig):
name = 'temporadas'

View file

@ -1,13 +0,0 @@
from django import forms
from .models import Temporada
class TemporadaForm(forms.ModelForm):
class Meta:
model = Temporada
fields = ['nombre', 'fecha_inicio', 'fecha_fin', 'presupuesto', 'actual', 'miembros']
widgets = {
'fecha_inicio': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'fecha_fin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'miembros': forms.CheckboxSelectMultiple(),
}

View file

@ -1,30 +0,0 @@
# Generated by Django 6.0.2 on 2026-02-17 11:28
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Temporada',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=50, unique=True)),
('fecha_inicio', models.DateField()),
('fecha_fin', models.DateField()),
('presupuesto', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)),
('actual', models.BooleanField(default=False, verbose_name='¿Es la temporada actual?')),
],
options={
'verbose_name': 'Temporada',
'verbose_name_plural': 'Temporadas',
'ordering': ['-fecha_inicio'],
},
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 6.0.2 on 2026-03-06 19:29
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('temporadas', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='temporada',
name='miembros',
field=models.ManyToManyField(blank=True, related_name='temporadas_participadas', to=settings.AUTH_USER_MODEL, verbose_name='Miembros del equipo'),
),
]

View file

@ -1,39 +0,0 @@
from django.db import models
from django.conf import settings
class Temporada(models.Model):
# El nombre será algo como "Gades 2024-25"
nombre = models.CharField(max_length=50, unique=True)
fecha_inicio = models.DateField()
fecha_fin = models.DateField()
# Presupuesto total para ese año
presupuesto = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
# Checkbox para marcar cuál es la temporada que estamos viviendo ahora
actual = models.BooleanField(default=False, verbose_name="¿Es la temporada actual?")
# Relaciones
# Relacion N a N con miembros
miembros = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name="temporadas_participadas",
blank=True,
verbose_name="Miembros del equipo"
)
class Meta:
verbose_name = "Temporada"
verbose_name_plural = "Temporadas"
ordering = ['-fecha_inicio'] # Ordena las más nuevas primero
def __str__(self):
return self.nombre
def save(self, *args, **kwargs):
# TRUCO PRO: Si marco esta temporada como "actual", desmarco todas las demás
# Así evitamos que haya dos temporadas activas a la vez.
if self.actual:
Temporada.objects.filter(actual=True).exclude(pk=self.pk).update(actual=False)
super().save(*args, **kwargs)

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,42 +0,0 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.views.decorators.http import require_POST
from users.decorators import require_rol
from .models import Temporada
from .forms import TemporadaForm
@require_rol('directiva')
def gestion_temporadas(request):
temporadas = Temporada.objects.all()
return render(request, 'gestion_temporadas.html', {'temporadas': temporadas})
@require_rol('directiva')
def crear_temporada(request):
form = TemporadaForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
form.save()
messages.success(request, 'Temporada creada correctamente.')
return redirect('gestion_temporadas')
return render(request, 'editar_temporada.html', {'form': form, 'temporada': None})
@require_rol('directiva')
def editar_temporada(request, pk):
temporada = get_object_or_404(Temporada, pk=pk)
form = TemporadaForm(request.POST or None, instance=temporada)
if request.method == 'POST' and form.is_valid():
form.save()
messages.success(request, 'Temporada actualizada correctamente.')
return redirect('gestion_temporadas')
return render(request, 'editar_temporada.html', {'form': form, 'temporada': temporada})
@require_rol('directiva')
@require_POST
def eliminar_temporada(request, pk):
temporada = get_object_or_404(Temporada, pk=pk)
temporada.delete()
messages.success(request, f'Temporada "{temporada.nombre}" eliminada.')
return redirect('gestion_temporadas')

View file

@ -1,27 +0,0 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
list_display = ('username', 'first_name', 'last_name', 'rol', 'especialidad', 'is_staff')
# Filtramos por rol y área técnica
list_filter = ('rol', 'especialidad', 'is_staff', 'is_active')
# Buscador de usuarios
search_fields = ('username', 'first_name', 'last_name', 'email')
# Para editar al usuario con los datos insertados
fieldsets = UserAdmin.fieldsets + (
('Información del Equipo Gades', {
'fields': ('rol', 'especialidad'),
}),
)
# Para crear al usuario con los datos insertados
add_fieldsets = UserAdmin.add_fieldsets + (
('Información del Equipo Gades', {
'fields': ('rol', 'especialidad'),
}),
)

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'

View file

@ -1,7 +0,0 @@
from django.contrib.auth.decorators import login_required, user_passes_test
def require_rol(*roles):
def decorator(view_func):
return login_required(user_passes_test(lambda u: u.rol in roles)(view_func))
return decorator

View file

@ -1,42 +0,0 @@
from django import forms
from .models import CustomUser
class PerfilForm(forms.ModelForm):
class Meta:
model = CustomUser
fields = ['username', 'first_name', 'last_name', 'email']
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'}),
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
}
labels = {
'username': 'Usuario',
'first_name': 'Nombre',
'last_name': 'Apellidos',
'email': 'Correo electrónico',
}
class EditarUsuarioForm(forms.ModelForm):
class Meta:
model = CustomUser
fields = ['username', 'first_name', 'last_name', 'email', 'rol', 'especialidad']
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'}),
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'rol': forms.Select(attrs={'class': 'form-select'}),
'especialidad': forms.Select(attrs={'class': 'form-select'}),
}
labels = {
'username': 'Usuario',
'first_name': 'Nombre',
'last_name': 'Apellidos',
'email': 'Correo electrónico',
'rol': 'Rol en el equipo',
'especialidad': 'Área técnica',
}

View file

@ -1,46 +0,0 @@
# Generated by Django 6.0.2 on 2026-02-17 10:36
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('rol', models.CharField(choices=[('directiva', 'Directiva'), ('jefe_area', 'Jefe de Área'), ('empleado', 'Empleado')], default='empleado', max_length=20)),
('especialidad', models.CharField(blank=True, choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software')], max_length=30, null=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 6.0.2 on 2026-02-17 11:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='especialidad',
field=models.CharField(blank=True, choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software')], max_length=30, null=True, verbose_name='Área Técnica'),
),
migrations.AlterField(
model_name='customuser',
name='rol',
field=models.CharField(choices=[('directiva', 'Directiva'), ('jefe_area', 'Jefe de Área'), ('empleado', 'Empleado')], default='empleado', max_length=20, verbose_name='Rol en el equipo'),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 6.0.2 on 2026-03-06 19:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_alter_customuser_especialidad_alter_customuser_rol'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='especialidad',
field=models.CharField(blank=True, choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business_operations', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software')], max_length=30, null=True, verbose_name='Área Técnica'),
),
migrations.AlterField(
model_name='customuser',
name='rol',
field=models.CharField(choices=[('directiva', 'Directiva'), ('jefe_area', 'Jefe de Área'), ('miembro', 'Miembro')], default='miembro', max_length=20, verbose_name='Rol en el equipo'),
),
]

View file

@ -1,43 +0,0 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
# --- 1. OPCIONES (El Menú) ---
ROL_CHOICES = (
('directiva', 'Directiva'),
('jefe_area', 'Jefe de Área'),
('miembro', 'Miembro'),
)
ESPECIALIDAD_CHOICES = (
('aerodinamica', 'Aerodinámica'),
('chasis', 'Chasis'),
('business_operations', 'Business & Operations'),
('epowertrain', 'E-Powertrain'),
('electronica', 'Electrónica'),
('sdf', 'SDF'),
('motor_transmision', 'Motor & Transmisión'),
('software', 'Software'),
)
# --- 2. CAMPOS (Las Columnas en la BD) ---
# Usamos la versión con 'verbose_name' porque queda mejor en la web
rol = models.CharField(
max_length=20,
choices=ROL_CHOICES,
default='miembro',
verbose_name="Rol en el equipo"
)
especialidad = models.CharField(
max_length=30,
choices=ESPECIALIDAD_CHOICES,
null=True,
blank=True,
verbose_name="Área Técnica"
)
# --- 3. MÉTODOS ---
def __str__(self):
# Muestra: "Nombre Apellido (usuario) - Rol"
return f"{self.first_name} {self.last_name} ({self.username}) - {self.get_rol_display()}"

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,119 +0,0 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
from django.contrib import messages
from django.views.decorators.http import require_POST
from .decorators import require_rol
from .forms import PerfilForm, EditarUsuarioForm
from .models import CustomUser
@login_required
def index(request):
return render(request, 'index.html')
@login_required
def mi_perfil(request):
perfil_form = PerfilForm(instance=request.user)
password_form = PasswordChangeForm(user=request.user)
if request.method == 'POST':
if 'guardar_perfil' in request.POST:
perfil_form = PerfilForm(request.POST, instance=request.user)
if perfil_form.is_valid():
perfil_form.save()
messages.success(request, 'Tus datos se han actualizado correctamente.')
return redirect('mi_perfil')
elif 'cambiar_password' in request.POST:
password_form = PasswordChangeForm(user=request.user, data=request.POST)
if password_form.is_valid():
user = password_form.save()
update_session_auth_hash(request, user)
messages.success(request, 'Tu contraseña se ha cambiado correctamente.')
return redirect('mi_perfil')
context = {
'perfil_form': perfil_form,
'password_form': password_form,
}
return render(request, 'mi_perfil.html', context)
@login_required
def listado_miembros(request):
miembros = CustomUser.objects.all().order_by('last_name', 'first_name')
rol = request.GET.get('rol', '')
especialidad = request.GET.get('especialidad', '')
apellido = request.GET.get('apellido', '')
if rol:
miembros = miembros.filter(rol=rol)
if especialidad:
miembros = miembros.filter(especialidad=especialidad)
if apellido:
miembros = miembros.filter(last_name__icontains=apellido)
context = {
'miembros': miembros,
'rol_choices': CustomUser.ROL_CHOICES,
'especialidad_choices': CustomUser.ESPECIALIDAD_CHOICES,
'filtro_rol': rol,
'filtro_especialidad': especialidad,
'filtro_apellido': apellido,
}
return render(request, 'listado_miembros.html', context)
@require_rol('directiva')
def gestion_usuarios(request):
usuarios = CustomUser.objects.all().order_by('last_name', 'first_name')
rol = request.GET.get('rol', '')
especialidad = request.GET.get('especialidad', '')
apellido = request.GET.get('apellido', '')
if rol:
usuarios = usuarios.filter(rol=rol)
if especialidad:
usuarios = usuarios.filter(especialidad=especialidad)
if apellido:
usuarios = usuarios.filter(last_name__icontains=apellido)
context = {
'usuarios': usuarios,
'rol_choices': CustomUser.ROL_CHOICES,
'especialidad_choices': CustomUser.ESPECIALIDAD_CHOICES,
'filtro_rol': rol,
'filtro_especialidad': especialidad,
'filtro_apellido': apellido,
}
return render(request, 'gestion_usuarios.html', context)
@require_rol('directiva')
def editar_usuario(request, pk):
usuario = get_object_or_404(CustomUser, pk=pk)
if request.method == 'POST':
form = EditarUsuarioForm(request.POST, instance=usuario)
if form.is_valid():
form.save()
messages.success(request, 'El usuario se ha actualizado correctamente.')
return redirect('gestion_usuarios')
else:
form = EditarUsuarioForm(instance=usuario)
return render(request, 'editar_usuario.html', {'form': form, 'usuario': usuario})
@require_rol('directiva')
@require_POST
def eliminar_usuario(request, pk):
usuario = get_object_or_404(CustomUser, pk=pk)
usuario.delete()
messages.success(request, 'El usuario se ha eliminado correctamente.')
return redirect('gestion_usuarios')