219 lines
9.0 KiB
Python
219 lines
9.0 KiB
Python
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 style='color: #0070f3; text-transform: uppercase;'>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"])
|
|
|
|
# Mostrar el título encima de la pregunta
|
|
content = f'''
|
|
<div style="margin-bottom: 15px;">
|
|
<span style="font-size: 0.85rem; font-weight: bold; color: #0070f3; text-transform: uppercase; letter-spacing: 0.5px;">{poll["titulo"]}</span>
|
|
<h2 style="margin-top: 5px; margin-bottom: 0;">{poll["pregunta"]}</h2>
|
|
</div>
|
|
'''
|
|
|
|
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)
|