Encuestas/app.py

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)