Version 1.2 Totalmente operativa

This commit is contained in:
luisgulo 2026-07-21 14:27:44 +02:00
parent 1c3506c049
commit dac9b502d4
8 changed files with 292 additions and 0 deletions

5
Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir flask
EXPOSE 8080
CMD ["python", "app.py"]

212
app.py Normal file
View File

@ -0,0 +1,212 @@
import os
import configparser
from flask import Flask, request, render_template_string, redirect, url_for
app = Flask(__name__)
DATA_DIR = "/data"
def get_poll_files():
if not os.path.exists(DATA_DIR):
return []
return [f for f in os.listdir(DATA_DIR) if f.endswith('.encuesta')]
def load_poll(filename):
filepath = os.path.join(DATA_DIR, filename)
if not os.path.exists(filepath):
return None
config = configparser.ConfigParser()
config.read(filepath, encoding='utf-8')
poll_id = filename.replace('.encuesta', '')
titulo = config.get('encuesta', 'titulo', fallback=poll_id)
pregunta = config.get('encuesta', 'pregunta', fallback='')
# Leer el estado de la encuesta (por defecto True si no existe el campo)
activa_raw = config.get('encuesta', 'activa', fallback='true').lower()
activa = activa_raw in ['true', '1', 'yes', 'si']
options = []
votes = []
if 'opciones' in config and 'votos' in config:
for key in config['opciones']:
options.append(config['opciones'][key])
votes.append(int(config['votos'].get(key, '0')))
return {
"id": poll_id,
"filename": filename,
"titulo": titulo,
"pregunta": pregunta,
"activa": activa,
"options": options,
"votes": votes
}
def save_vote(filename, option_index):
filepath = os.path.join(DATA_DIR, filename)
config = configparser.ConfigParser()
config.read(filepath, encoding='utf-8')
# Seguridad: verificar si sigue activa antes de guardar
activa_raw = config.get('encuesta', 'activa', fallback='true').lower()
if activa_raw not in ['true', '1', 'yes', 'si']:
return False
key = str(option_index + 1)
current_votes = int(config.get('votos', key, fallback='0'))
config.set('votos', key, str(current_votes + 1))
with open(filepath, 'w', encoding='utf-8') as f:
config.write(f)
return True
HTML_LAYOUT = """
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Encuestas</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; background: #f4f6f8; padding: 20px; display: flex; justify-content: center; }
.card { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); max-width: 550px; width: 100%; }
h2 { color: #1a252f; margin-bottom: 15px; font-size: 1.2rem; }
.section-title { font-size: 1.1rem; color: #495057; border-bottom: 2px solid #e9ecef; padding-bottom: 8px; margin-top: 25px; margin-bottom: 15px; }
.option { margin-bottom: 12px; }
button, .btn { display: inline-block; text-align: center; width: 100%; padding: 10px; background: #0070f3; color: white; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; text-decoration: none; box-sizing: border-box; }
button:hover, .btn:hover { background: #0051a8; }
.btn-secondary { background: #6c757d; }
.btn-secondary:hover { background: #5a6268; }
.bar-container { background: #e9ecef; border-radius: 6px; overflow: hidden; margin-top: 5px; height: 24px; position: relative; }
.bar { background: #10b981; height: 100%; transition: width 0.3s; }
.bar-closed { background: #6b7280; }
.bar-text { position: absolute; right: 10px; top: 2px; font-size: 0.85rem; font-weight: bold; color: #333; }
.poll-item { background: #f8f9fa; padding: 12px; border-radius: 6px; margin-bottom: 10px; border-left: 4px solid #0070f3; }
.poll-item.closed { border-left-color: #6c757d; opacity: 0.9; }
.badge { display: inline-block; padding: 3px 8px; font-size: 0.75rem; font-weight: bold; border-radius: 4px; color: white; margin-bottom: 5px; }
.badge-active { background-color: #10b981; }
.badge-closed { background-color: #6c757d; }
</style>
</head>
<body>
<div class="card">
{{ content | safe }}
</div>
</body>
</html>
"""
@app.route('/')
def home():
files = get_poll_files()
active_polls = []
closed_polls = []
for f in files:
poll = load_poll(f)
if poll:
if poll["activa"]:
active_polls.append(poll)
else:
closed_polls.append(poll)
content = "<h1>SoloConLinux: Panel de Encuestas </h1>"
# BLOQUE: Encuestas Activas
content += '<div class="section-title"><b>Encuestas Activas</b></div>'
if not active_polls:
content += "<p style='color: #666; font-size: 0.9rem;'>No hay encuestas activas en este momento.</p>"
else:
for poll in active_polls:
content += f'''
<div class="poll-item">
<span class="badge badge-active">ACTIVA</span><br>
<strong>{poll["titulo"]}</strong><br>
<small style="color: #666;">{poll["pregunta"]}</small><br><br>
<a href="/poll/{poll["id"]}" class="btn" style="width: auto; padding: 5px 12px; font-size: 0.85rem;">Votar / Resultados</a>
</div>
'''
# BLOQUE: Encuestas Finalizadas
content += '<div class="section-title"><b>Encuestas Finalizadas</b></div>'
if not closed_polls:
content += "<p style='color: #666; font-size: 0.9rem;'>No hay encuestas finalizadas.</p>"
else:
for poll in closed_polls:
content += f'''
<div class="poll-item closed">
<span class="badge badge-closed">FINALIZADA</span><br>
<strong>{poll["titulo"]}</strong><br>
<small style="color: #666;">{poll["pregunta"]}</small><br><br>
<a href="/poll/{poll["id"]}" class="btn btn-secondary" style="width: auto; padding: 5px 12px; font-size: 0.85rem;">Ver Resultados</a>
</div>
'''
return render_template_string(HTML_LAYOUT, content=content)
@app.route('/poll/<poll_id>')
def show_poll(poll_id):
filename = f"{poll_id}.encuesta"
poll = load_poll(filename)
if not poll:
return "Encuesta no encontrada", 404
# Si la encuesta NO está activa, forzar siempre la vista de resultados
voted = (request.args.get('voted') == '1') or (not poll["activa"])
content = f'<h2>{poll["pregunta"]}</h2>'
if not poll["activa"]:
content += '<p style="color: #dc2626; font-weight: bold; font-size: 0.9rem;">⚠️ Esta encuesta ha finalizado. Solo se pueden consultar los resultados.</p>'
if voted:
total = sum(poll["votes"])
content += "<p><strong>Resultados:</strong></p>"
bar_class = "bar" if poll["activa"] else "bar bar-closed"
for i, opt in enumerate(poll["options"]):
count = poll["votes"][i]
percent = round((count / total * 100), 1) if total > 0 else 0
content += f'''
<div style="margin-bottom: 15px;">
<div>{opt}</div>
<div class="bar-container">
<div class="bar {bar_class}" style="width: {percent}%;"></div>
<span class="bar-text">{count} votos ({percent}%)</span>
</div>
</div>
'''
content += f'<p style="text-align: center; color: #666; font-size: 0.85rem;">Total de votos: {total}</p>'
content += f'<a href="/" class="btn btn-secondary">Volver al listado</a>'
else:
content += f'<form action="/vote/{poll["id"]}" method="POST">'
for i, opt in enumerate(poll["options"]):
content += f'''
<div class="option">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="radio" name="option" value="{i}" required> {opt}
</label>
</div>
'''
content += '<button type="submit" style="margin-top: 15px;">Votar</button>'
content += f'<br><br><a href="/poll/{poll["id"]}?voted=1" style="color: #666; font-size: 0.85rem; display: block; text-align: center;">Ver resultados sin votar</a>'
content += '</form>'
return render_template_string(HTML_LAYOUT, content=content)
@app.route('/vote/<poll_id>', methods=['POST'])
def vote(poll_id):
filename = f"{poll_id}.encuesta"
poll = load_poll(filename)
# Si la encuesta está desactivada, ignorar el intento de voto
if not poll or not poll["activa"]:
return redirect(url_for('show_poll', poll_id=poll_id))
idx = int(request.form.get('option'))
save_vote(filename, idx)
return redirect(url_for('show_poll', poll_id=poll_id, voted=1))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)

2
build.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
docker build -t encuestas .

View File

@ -0,0 +1,17 @@
[encuesta]
titulo = Publicaciones en SoloConLinux
pregunta = ¿Qué opináis sobre crear este tipo de publicación en SoloConLinux?
activa = true
[opciones]
1 = Es algo interesante
2 = Interesante pero mejor 2 ediciones anuales
3 = No tiene sentido hoy en día
4 = Prefiero directamente el Blog de soloconlinux.org.es
[votos]
1 = 3
2 = 1
3 = 0
4 = 1

View File

@ -0,0 +1,19 @@
[encuesta]
titulo = Elección de distribución en servidores
pregunta = ¿Qué distribución de Linux prefieres utilizar para desplegar servicios en producción?
activa = false
[opciones]
1 = <b>Debian</b> (Estable y puro)
2 = <b>Ubuntu Server</b> (Soporte extendido y ecosistema)
3 = <b>AlmaLinux / Rocky Linux</b> (Entorno empresarial tipo RHEL)
4 = <b>Alpine Linux</b> (Imágenes ultra ligeras para contenedores)
5 = <b>Arch Linux</b> (Para estar siempre a la última)
[votos]
1 = 1324
2 = 340
3 = 564
4 = 340
5 = 450

20
plantilla.encuesta Normal file
View File

@ -0,0 +1,20 @@
[encuesta]
titulo = Titulo de la Encuesta
pregunta = ¿Qué bla bla bla bla prefieres?
# activa = true|false
activa = true
[opciones]
# n = texto de la opcion
1 = Texto de la opcion 1
2 = Texto de la opcion 2
3 = Texto de la opcion 3
4 = Texto de la opcion 4
[votos]
# inicializar con valor 0
1 = 0
2 = 0
3 = 0
4 = 0

12
run.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
docker rm -f encuestas 2> /dev/null
docker run -d \
--name encuestas \
--restart always \
-p 8080:8080 \
-v ${PWD}/app.py:/app/app.py \
-v ${PWD}/encuestas:/data \
encuestas
echo "Abra el Gestor de Encuestas en: http://localhost:8080"

5
stop.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
echo "Deteniendo contenedor de Encuestas..."
docker rm -f encuestas 2> /dev/null
echo "¡Detenido!"
echo