Version 1.2 Totalmente operativa
This commit is contained in:
parent
1c3506c049
commit
dac9b502d4
5
Dockerfile
Normal file
5
Dockerfile
Normal 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
212
app.py
Normal 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)
|
||||
17
encuestas/01-Publicaciones_Soloconlinux.encuesta
Normal file
17
encuestas/01-Publicaciones_Soloconlinux.encuesta
Normal 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
|
||||
|
||||
19
encuestas/02-Eleccion servidor.encuesta
Normal file
19
encuestas/02-Eleccion servidor.encuesta
Normal 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
20
plantilla.encuesta
Normal 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
12
run.sh
Executable 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"
|
||||
Loading…
x
Reference in New Issue
Block a user