Initial commit. Aka, everything

This commit is contained in:
Nacho 2025-12-17 12:09:23 +00:00
parent 6c8876a280
commit 7a33f360cf
16 changed files with 824 additions and 0 deletions

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"python-envs.defaultEnvManager": "ms-python.python:conda",
"python-envs.defaultPackageManager": "ms-python.python:conda",
"python-envs.pythonProjects": []
}

16
admin/__init__.py Normal file
View File

@ -0,0 +1,16 @@
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from models import (
Usuario,
Taquilla,
Alquiler,
Seccion,
db
)
admin = Admin()
admin.add_view(ModelView(Usuario, db.session))
admin.add_view(ModelView(Taquilla, db.session))
admin.add_view(ModelView(Seccion, db.session))
admin.add_view(ModelView(Alquiler, db.session))

141
endpoints/alquileres.py Normal file
View File

@ -0,0 +1,141 @@
from flask import (
Blueprint,
render_template,
request,
redirect
)
from models import (
Alquiler,
Usuario,
Taquilla,
db
)
import datetime
from dateutil.relativedelta import relativedelta
alquileres = Blueprint(
"gestion_alquileres",
__name__
)
@alquileres.route("/ver-alquileres")
def ver_alquileres(info = None):
alquileres = (
db.session.query(
Usuario.id,
Usuario.nombre,
Usuario.dni,
Taquilla.id,
Taquilla.id_taquilla,
Alquiler.validez
)
.join(Alquiler, Alquiler.id_usuario == Usuario.id)
.join(Taquilla, Alquiler.id_taquilla == Taquilla.id)
.all()
)
alquileres = [
{
"id_usuario": id_usuario,
"id_taquilla": id_taquilla,
"nombre_persona": nombre,
"dni_persona": dni,
"taquilla": numero_taquilla,
"validez": datetime.datetime.fromtimestamp(int(validez)).strftime("%d/%m/%Y %H:%M:%S")
} for
id_usuario,
nombre,
dni,
id_taquilla,
numero_taquilla,
validez
in alquileres
]
return render_template("ver_alquileres.html", alquileres=alquileres, info=info)
@alquileres.route("/renovar/<id_taquilla>")
def renovar_alquiler(id_taquilla):
if not id_taquilla:
return redirect("/")
alquiler = (
db.session.query(Alquiler)
.filter(Alquiler.id_taquilla == id_taquilla)
.first()
)
current_date = datetime.datetime.now()
validez = current_date + relativedelta(months=3)
validez = validez.replace(hour=23, minute=59, second=0, microsecond=0)
validez = int(validez.timestamp())
alquiler.validez = validez
db.session.commit()
usuario = (
db.session.query(Usuario)
.filter(Usuario.id == alquiler.id_usuario)
.first()
)
return ver_alquileres(info=f"Alquiler renovado correctamente para {usuario.nombre}")
@alquileres.route("/confirmar-baja/<id_taquilla>")
def confirmar_baja(id_taquilla):
info_taquilla = (
db.session.query(
Taquilla.id_taquilla,
Usuario.nombre
)
.join(Alquiler, Alquiler.id_taquilla == Taquilla.id)
.join(Usuario, Alquiler.id_usuario == Usuario.id)
.filter(Taquilla.id == id_taquilla)
.first()
)
info_taquilla = {
"numero_taquilla": info_taquilla[0],
"nombre_persona": info_taquilla[1]
}
return render_template(
"confirmar_baja.html",
info_taquilla=info_taquilla,
id_taquilla=id_taquilla
)
@alquileres.route("/baja/<id_taquilla>")
def dar_baja(id_taquilla):
alquiler = (
db.session.query(Alquiler)
.filter(Alquiler.id_taquilla == id_taquilla)
.first()
)
usuario = (
db.session.query(Usuario)
.filter(Usuario.id == alquiler.id_usuario)
.first()
)
taquilla = (
db.session.query(Taquilla)
.filter(Taquilla.id == id_taquilla)
.first()
)
if alquiler and usuario and taquilla:
nombre_usuario = usuario.nombre
id_taquilla = taquilla.id_taquilla
db.session.delete(alquiler)
db.session.delete(usuario)
db.session.commit()
return ver_alquileres(info=f"Se ha dado de baja el alquiler para {nombre_usuario} en la taquilla {id_taquilla}")
else:
return ver_alquileres(info="No existe este alquiler")

61
endpoints/retrasos.py Normal file
View File

@ -0,0 +1,61 @@
from flask import (
Blueprint,
render_template
)
from models import (
Usuario,
Taquilla,
Alquiler,
db
)
import datetime
retrasos = Blueprint(
"gestion_retrasos",
__name__
)
@retrasos.route("/ver-retrasos")
def ver_retrasos(info = None):
current_date = int(datetime.datetime.now().timestamp())
alquileres = (
db.session.query(
Usuario.id,
Usuario.nombre,
Usuario.dni,
Taquilla.id,
Taquilla.id_taquilla,
Alquiler.validez
)
.join(Alquiler, Alquiler.id_usuario == Usuario.id)
.join(Taquilla, Alquiler.id_taquilla == Taquilla.id)
.filter(Alquiler.validez < current_date)
.all()
)
alquileres = [
{
"id_usuario": id_usuario,
"id_taquilla": id_taquilla,
"nombre_persona": nombre,
"dni_persona": dni,
"taquilla": numero_taquilla,
"validez": datetime.datetime.fromtimestamp(int(validez)).strftime("%d/%m/%Y %H:%M:%S")
} for
id_usuario,
nombre,
dni,
id_taquilla,
numero_taquilla,
validez
in alquileres
]
if len(alquileres) > 0:
return render_template("ver_alquileres.html", alquileres=alquileres, info=info)
else:
return render_template("ver_alquileres.html", alquileres=alquileres, info="No existe ningún alquiler expirado.")

138
endpoints/taquillas.py Normal file
View File

@ -0,0 +1,138 @@
from flask import (
Blueprint,
render_template,
request
)
from models import (
Seccion,
Taquilla,
Alquiler,
Usuario,
db
)
import time
import datetime
from dateutil.relativedelta import relativedelta
taquillas = Blueprint(
"gestion_taquillas",
__name__
)
@taquillas.route("/secciones")
def ver_secciones():
secciones = db.session.query(
Seccion
)
return render_template(
"ver_secciones.html",
secciones=secciones
)
@taquillas.route("/secciones/<id_seccion>")
def ver_taquillas_seccion(id_seccion):
taquillas = (
db.session.query(Taquilla.id_taquilla, Alquiler.id_usuario)
.join(Alquiler, Taquilla.id == Alquiler.id_taquilla, isouter=True)
.filter(Taquilla.id_seccion == int(id_seccion))
.all()
)
print(taquillas)
taquillas_result = [
{
"id_taquilla": id_taquilla,
"alquilado": id_usuario is not None
} for id_taquilla, id_usuario in taquillas
]
return render_template(
"ver_taquillas.html",
taquillas=taquillas_result
)
@taquillas.route("/alquilar/<id_taquilla>", methods=["GET", "POST"])
def alquilar_taquilla(id_taquilla):
if request.method == "GET":
return render_template(
"alquilar_taquilla.html",
id_taquilla=id_taquilla
)
else:
nombre = request.form.get("nombre_usuario").upper()
dni = request.form.get("dni_usuario").upper()
correo = request.form.get("correo_electronico")
agreement = request.form.get("agreement")
print(nombre, dni, correo, agreement)
error = None
if not (dni and correo and nombre):
error = "Faltan datos."
if not agreement:
error = "No está de acuerdo con las políticas."
user = (
db.session.query(Usuario)
.filter(Usuario.dni == dni)
.first()
)
if not user:
user = Usuario()
user.correo = correo
user.dni = dni
user.nombre = nombre
db.session.add(user)
alquiler_user = (
db.session.query(Alquiler)
.filter(Alquiler.id_usuario == user.id)
.first()
)
if alquiler_user:
error = "Usuario ya tiene una taquilla alquilada. Debe renovar o dar de baja el alquiler."
taquilla = (
db.session.query(Taquilla)
.filter(Taquilla.id_taquilla == id_taquilla)
.first()
)
if not taquilla:
error = "La taquilla en cuestión no existe"
if not error:
alquiler_user = Alquiler()
alquiler_user.id_usuario = user.id
alquiler_user.id_taquilla = taquilla.id
current_date = datetime.datetime.now()
validez = current_date + relativedelta(months=3)
validez = validez.replace(hour=23, minute=59, second=0, microsecond=0)
alquiler_user.validez = int(validez.timestamp())
db.session.add(alquiler_user)
db.session.commit()
return render_template(
"pagina_principal.html",
info=f"¡La taquilla {id_taquilla} ha sido alquilada a {nombre}!"
)
else:
print(error)
return render_template(
"alquilar_taquilla.html",
id_taquilla=id_taquilla,
info=error
)

38
main.py Normal file
View File

@ -0,0 +1,38 @@
from flask import (
Flask,
render_template
)
from models import db
from admin import admin
from endpoints.taquillas import taquillas
from endpoints.alquileres import alquileres
from endpoints.retrasos import retrasos
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///project.db"
app.secret_key = "hola"
db.init_app(app)
app.register_blueprint(taquillas)
app.register_blueprint(alquileres)
app.register_blueprint(retrasos)
@app.route("/")
def index():
return render_template("pagina_principal.html")
with app.app_context():
db.create_all()
if __name__ == "__main__":
admin.init_app(app)
app.run(
host="0.0.0.0",
port=5000,
debug=True
)

42
models/__init__.py Normal file
View File

@ -0,0 +1,42 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Seccion(db.Model):
__tablename__ = "seccion"
id = db.Column(db.Integer, primary_key=True)
nombre = db.Column(db.String)
descripcion = db.Column(db.String)
class Taquilla(db.Model):
__tablename__ = "taquilla"
id = db.Column(db.Integer, primary_key=True)
id_taquilla = db.Column(db.Integer)
id_seccion = db.Column(db.Integer, db.ForeignKey("seccion.id"))
class Usuario(db.Model):
__tablename__ = "usuario"
id = db.Column(db.Integer, primary_key=True)
nombre = db.Column(db.String)
dni = db.Column(db.String)
correo = db.Column(db.String)
class Alquiler(db.Model):
__tablename__ = "alquiler"
id_taquilla = db.Column(
db.Integer,
db.ForeignKey("taquilla.id"),
primary_key=True
)
id_usuario = db.Column(
db.Integer,
db.ForeignKey("usuario.id"),
primary_key=True
)
validez = db.Column(
db.Integer
)

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
flask
flask-login
flask-admin
flask-sqlalchemy
python-dateutil

181
static/index.css Normal file
View File

@ -0,0 +1,181 @@
h1, a, p, label {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
}
nav {
background-color: rgb(32, 32, 90);
width: calc(100% - 8px);
height: 24px;
display: flex;
align-items: center;
padding: 4px;
border-radius: 0px 0px 4px 4px;
}
a.title, a.title:active {
color: white;
text-decoration: none;
}
.nav-items {
display: flex;
align-items: center;
gap: 8px;
}
.content {
margin: 8px;
overflow-x: auto;
overflow-y: hidden;
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
.slider {
display: flex;
gap: 8px;
height: 100%;
flex-shrink: 0;
gap: 4px;
flex-wrap: wrap;
}
.slider-item {
border-radius: 4px;
display: flex;
flex-direction: column;
width: 256px;
height: 256px;
border: 1px solid gray;
align-items: center;
justify-content: center;
text-align: center;
}
.grid {
display: flex;
gap: 8px;
height: 100%;
overflow-y: auto;
flex-wrap: wrap;
align-content: flex-start;
justify-content: center;
}
.grid-item {
border-radius: 4px;
display: flex;
flex-direction: column;
width: 128px;
height: 64px;
border: 1px solid gray;
align-items: center;
justify-content: center;
text-align: center;
}
.grid-item:hover, .slider-item:hover, .disabled {
background-color: rgb(128, 128, 128);
}
.form {
display: flex;
flex-direction: column;
gap: 16px;
background-color: #e0e0e0;
border-radius: 8px;
padding: 16px;
border: 1px solid #e1e1e1;
}
input[type=text] {
padding: 8px;
font-size: 24px;
border-radius: 8px;
border: 0px
}
.form-element {
display: flex;
flex-direction: column;
}
.form-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.button {
padding: 8px;
font-size: 16px;
border-radius: 8px;
border: 0px;
background-color: #c0c0c0;
}
.form-submit {
display: flex;
justify-content: space-between;
width: 100%;
}
.info {
padding: 8px;
background-color: #bbccff;
border-radius: 8px;
border: 2px solid #99aaff;
margin: 8px;
}
table {
/* Box Model & Layout */
width: 100%; /* Make the table take up the full width of its container */
border-collapse: collapse; /* Essential: Merges the borders of adjacent cells into a single line */
font-family: Arial, sans-serif; /* A clean, readable font */
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */
}
/* Styling the Table Header */
th {
background-color: #007bff; /* Primary color background for the header */
color: white; /* White text for contrast */
text-align: left; /* Align text to the left */
padding: 12px 15px; /* Comfortable padding */
}
/* Styling the Table Cells (Data and Headers) */
td, th {
border: 1px solid #dddddd; /* Light gray border for all cells */
padding: 10px 15px; /* Consistent padding */
}
/* Styling the Table Rows */
tr:nth-child(even) {
background-color: #f3f3f3; /* Zebra striping: light gray background for even rows */
}
tr:hover {
background-color: #e0e0e0; /* Highlight the row when the user hovers over it */
cursor: pointer; /* Change cursor to pointer to indicate interactivity */
}
/* Optional: Styling the Table Footer (if you use a <tfoot> element) */
tfoot td {
font-weight: bold;
background-color: #cccccc;
}
a {
color: blue
}

View File

@ -0,0 +1,50 @@
{% extends "base.html" %}
{% block title %}
Alquilar taquilla
{% endblock %}
{% block content %}
<div class="content">
<div class="form-content">
<form method="post" action="/alquilar/{{ id_taquilla }}" class="form">
{% if error %}
<div class="error-message">
{{ error }}
</div>
{% endif %}
<div class="form-element">
<label for="id_taquilla">Numero de la Taquilla</label>
<input type="text" name="id_taquilla" id="id_taquilla" placeholder="Número de la taquilla" value="{{ id_taquilla }}" disabled>
</div>
<div class="form-element">
<label for="nombre_usuario">Nombre completo</label>
<input type="text" name="nombre_usuario" id="nombre_usuario" placeholder="Nombre completo">
</div>
<div class="form-element">
<label for="dni_usuario">Numero DNI</label>
<input type="text" name="dni_usuario" id="dni_usuario" placeholder="Numero DNI">
</div>
<div class="form-element">
<label for="correo_electronico">Correo electrónico</label>
<input type="text" name="correo_electronico" id="correo_electronico" placeholder="Correo electrónico">
</div>
<div>
<input type="checkbox" id="agreement" name="agreement">
<label for="agreement">He leído la información correspondiente y estoy de acuerdo.</label>
</div>
<div class="form-submit">
<button onclick="history.back()" class="button">Atrás</button>
<input type="submit" class="button" value="Alquilar">
</div>
</form>
</div>
</div>
{% endblock %}

26
templates/base.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>NacaTaquillas | {% block title %}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="/static/index.css">
</head>
<body>
<nav>
<a class="title" href="{{ url_for('index') }}">NacaTaquillas</a>
</nav>
{% if info %}
<div class="info">
{{ info }}
</div>
{% endif %}
{% block content %}
{% endblock %}
</body>
{% block script %}
{% endblock %}
</html>

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block index %}
Confirmar baja
{% endblock %}
{% block content %}
<div class="content">
<div class="form-content">
<div class="form">
<p>¿Estás seguro de que quieres dar de baja la taquilla {{ info_taquilla.numero_taquilla }}, asociada a {{ info_taquilla.nombre_persona }}?</p>
<div class="form-submit">
<button onclick="history.back()" class="button">Atrás</button>
<button onclick="window.location = '/baja/{{ id_taquilla }}'" class="button">Confirmar</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}
Inicio
{% endblock %}
{% block content %}
<div class="content">
<!-- El Victor quería que se llamase NacaZulos, pero como el consejo no quería, lo dejo aqui guardado -->
<!-- Puto victor -->
<div class="slider-item" onclick="window.location = '{{ url_for("gestion_taquillas.ver_secciones") }}'">
<div>
<h1>Alquilar taquilla</h1>
</div>
<div>
<p>Alquilar una taquilla a un usuario</p>
</div>
</div>
<div class="slider-item" onclick="window.location = '{{ url_for("gestion_alquileres.ver_alquileres") }}'">
<h1>Ver alquileres</h1>
<p>Ver las taquillas alquiladas en una sección</p>
</div>
<div class="slider-item" onclick="window.location = '{{ url_for("gestion_retrasos.ver_retrasos") }}'">
<h1>Ver alquileres caducados</h1>
<p>Ver los alquileres que no se han renovado hasta la fecha actual</p>
</div>
<div class="slider-item" onclick="window.location = '/admin'">
<h1>Administración</h1>
<p>¡¡Funciones de administrador!!</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}
Alquileres
{% endblock %}
{% block content %}
<table>
<tr>
<th>ID de taquilla</th>
<th>Nombre de usuario</th>
<th>DNI</th>
<th>Validez</th>
<th>Acciones</th>
</tr>
{% for alquiler in alquileres %}
<tr>
<td>{{ alquiler.taquilla }}</td>
<td>{{ alquiler.nombre_persona }}</td>
<td>{{ alquiler.dni_persona }}</td>
<td>{{ alquiler.validez }}</td>
<td>
<a href="/renovar/{{ alquiler.id_taquilla }}">Renovar</a>
<a href="/confirmar-baja/{{ alquiler.id_taquilla }}">Baja</a>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}
Secciones
{% endblock %}
{% block content %}
<div class="content">
{% for seccion in secciones %}
<div class="slider-item" onclick="window.location = '{{ url_for('gestion_taquillas.ver_taquillas_seccion', id_seccion=seccion.id) }}'" class="slider-items">
<h1>{{ seccion.nombre }}</h1>
<p>{{ seccion.descripcion }}</p>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block title %}
Taquillas
{% endblock %}
{% block content %}
<div class="content">
<div class="grid">
{% for taquilla in taquillas %}
{% set desactivado = "disabled" if taquilla.alquilado else "" %}
<div onclick="window.location='/alquilar/{{ taquilla.id_taquilla}}'" class="grid-item {{ desactivado }}" {{ desactivado }}>
<h1>{{ taquilla.id_taquilla }}</h1>
</div>
{% endfor %}
</div>
</div>
{% endblock %}