Ficha de invitados: teléfono móvil obligatorio y aviso de envío por correo electrónico

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSyR5Zw5eGj6oPTRVjvP1b
This commit is contained in:
Alex
2026-10-21 00:16:56 +02:00
co-authored by Claude Sonnet 5.5
parent 6c3a572cdd
commit a25c7c256e
5 changed files with 16 additions and 5 deletions
+3
View File
@@ -30,6 +30,9 @@ def pregunta(q):
EMAIL = ''' <label><span>Tu correo electrónico <span class="req" title="Obligatoria">*</span></span><small>Te enviaremos un recordatorio de tu cita y así podemos responderte.</small> EMAIL = ''' <label><span>Tu correo electrónico <span class="req" title="Obligatoria">*</span></span><small>Te enviaremos un recordatorio de tu cita y así podemos responderte.</small>
<input name="email" type="email" autocomplete="email" maxlength="200" required> <input name="email" type="email" autocomplete="email" maxlength="200" required>
</label>
<label><span>Tu teléfono móvil <span class="req" title="Obligatoria">*</span></span><small>Solo para contactarte el día de la grabación si surge algún imprevisto.</small>
<input name="tel" type="tel" autocomplete="tel" inputmode="tel" maxlength="20" required>
</label>''' </label>'''
bloques = [] bloques = []
for s in secciones: for s in secciones:
+3
View File
@@ -42,6 +42,9 @@
<label><span>Tu correo electrónico <span class="req" title="Obligatoria">*</span></span><small>Te enviaremos un recordatorio de tu cita y así podemos responderte.</small> <label><span>Tu correo electrónico <span class="req" title="Obligatoria">*</span></span><small>Te enviaremos un recordatorio de tu cita y así podemos responderte.</small>
<input name="email" type="email" autocomplete="email" maxlength="200" required> <input name="email" type="email" autocomplete="email" maxlength="200" required>
</label> </label>
<label><span>Tu teléfono móvil <span class="req" title="Obligatoria">*</span></span><small>Solo para contactarte el día de la grabación si surge algún imprevisto.</small>
<input name="tel" type="tel" autocomplete="tel" inputmode="tel" maxlength="20" required>
</label>
<div class="nav"><button type="button" class="btn primary next">Siguiente →</button></div> <div class="nav"><button type="button" class="btn primary next">Siguiente →</button></div>
</fieldset> </fieldset>
<fieldset class="sec"> <fieldset class="sec">
+4 -2
View File
@@ -24,6 +24,8 @@
var el = els[j], bad = el.type === 'checkbox' ? !el.checked : !el.value.trim(); var el = els[j], bad = el.type === 'checkbox' ? !el.checked : !el.value.trim();
if (bad) { el.focus(); say(el.type === 'checkbox' ? 'Marca las dos casillas finales para poder enviar la ficha.' : 'Faltan respuestas obligatorias (las marcadas con *).', false); return false; } if (bad) { el.focus(); say(el.type === 'checkbox' ? 'Marca las dos casillas finales para poder enviar la ficha.' : 'Faltan respuestas obligatorias (las marcadas con *).', false); return false; }
} }
var t = s.querySelector('input[type=tel]');
if (t && !/^\+?[\d ().-]{7,20}$/.test(t.value.trim())) { t.focus(); say('El teléfono no parece válido.', false); return false; }
var m = s.querySelector('input[type=email]'); var m = s.querySelector('input[type=email]');
if (m && !okMail(m.value.trim())) { m.focus(); say('El correo no parece válido.', false); return false; } if (m && !okMail(m.value.trim())) { m.focus(); say('El correo no parece válido.', false); return false; }
return true; return true;
@@ -47,7 +49,7 @@
var d = new Date(j.cita * 1000), o = { timeZone: 'Europe/Madrid' }; var d = new Date(j.cita * 1000), o = { timeZone: 'Europe/Madrid' };
var dia = d.toLocaleDateString('es-ES', Object.assign({ weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }, o)).replace(',', ''); var dia = d.toLocaleDateString('es-ES', Object.assign({ weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }, o)).replace(',', '');
var hora = d.toLocaleTimeString('es-ES', Object.assign({ hour: '2-digit', minute: '2-digit' }, o)); var hora = d.toLocaleTimeString('es-ES', Object.assign({ hour: '2-digit', minute: '2-digit' }, o));
cita.textContent = '📅 Recuerda que grabamos el ' + dia + ' a las ' + hora + ', y te enviaremos por correo la dirección del estudio.'; cita.textContent = '📅 Recuerda que grabamos el ' + dia + ' a las ' + hora + ', y te enviaremos por correo electrónico la dirección del estudio.';
hola.hidden = false; cita.hidden = false; intro.hidden = false; f.hidden = false; hola.hidden = false; cita.hidden = false; intro.hidden = false; f.hidden = false;
try { var s = JSON.parse(store('get') || '{}'); Object.keys(s).forEach(function (nm) { if (f.elements[nm] && f.elements[nm].type !== 'checkbox') f.elements[nm].value = s[nm]; }); } catch (e) {} try { var s = JSON.parse(store('get') || '{}'); Object.keys(s).forEach(function (nm) { if (f.elements[nm] && f.elements[nm].type !== 'checkbox') f.elements[nm].value = s[nm]; }); } catch (e) {}
show(0); show(0);
@@ -73,7 +75,7 @@
Array.prototype.forEach.call(f.elements, function (el) { if (/^q\d+$/.test(el.name)) resp[el.name] = el.value.trim(); }); Array.prototype.forEach.call(f.elements, function (el) { if (/^q\d+$/.test(el.name)) resp[el.name] = el.value.trim(); });
busy = true; say('Enviando…', true); busy = true; say('Enviando…', true);
fetch('/api/invitados', { method: 'POST', headers: { 'Content-Type': 'application/json' }, fetch('/api/invitados', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ n: n, f: fe, email: f.email.value.trim(), resp: resp, consent: true, web: f.web.value || '', t: Date.now() - t0 }) }) body: JSON.stringify({ n: n, f: fe, email: f.email.value.trim(), tel: f.tel.value.trim(), resp: resp, consent: true, web: f.web.value || '', t: Date.now() - t0 }) })
.then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok && j.ok, j: j }; }); }) .then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok && j.ok, j: j }; }); })
.then(function (x) { .then(function (x) {
if (x.ok) { store('del'); f.reset(); steps.forEach(function (s) { s.hidden = true; }); paso.hidden = true; f.querySelector('.note:last-of-type').hidden = true; say(f.getAttribute('data-ok'), true); } if (x.ok) { store('del'); f.reset(); steps.forEach(function (s) { s.hidden = true; }); paso.hidden = true; f.querySelector('.note:last-of-type').hidden = true; say(f.getAttribute('data-ok'), true); }
+1 -1
View File
@@ -32,7 +32,7 @@
<p>Si nos escribes desde la página de <a href="contacto.html">contacto</a> o de <a href="patrocinadores.html">patrocinadores</a>, recogemos los datos que tú mismo introduces: nombre, correo electrónico, mensaje y motivo o tipo de colaboración y, en el formulario de patrocinio, también el nombre de la empresa y su web si la indicas. Los usamos <strong>únicamente para responderte</strong>. La base legal es tu consentimiento, que das al marcar la casilla del formulario.</p> <p>Si nos escribes desde la página de <a href="contacto.html">contacto</a> o de <a href="patrocinadores.html">patrocinadores</a>, recogemos los datos que tú mismo introduces: nombre, correo electrónico, mensaje y motivo o tipo de colaboración y, en el formulario de patrocinio, también el nombre de la empresa y su web si la indicas. Los usamos <strong>únicamente para responderte</strong>. La base legal es tu consentimiento, que das al marcar la casilla del formulario.</p>
<p>La web no guarda los mensajes: el formulario los reenvía por correo electrónico, a través de nuestro propio servidor de correo, a la cuenta del programa. Los conservamos solo el tiempo necesario para atender tu consulta y no los cedemos a terceros ni los usamos para otra cosa. Los formularios incluyen medidas antispam que no identifican a nadie. Puedes pedirnos que borremos tus datos escribiendo al correo de contacto.</p> <p>La web no guarda los mensajes: el formulario los reenvía por correo electrónico, a través de nuestro propio servidor de correo, a la cuenta del programa. Los conservamos solo el tiempo necesario para atender tu consulta y no los cedemos a terceros ni los usamos para otra cosa. Los formularios incluyen medidas antispam que no identifican a nadie. Puedes pedirnos que borremos tus datos escribiendo al correo de contacto.</p>
<p><strong>Ficha del invitado.</strong> Si participas como invitado y rellenas la ficha (a la que se accede solo mediante un enlace personal), recogemos las respuestas que escribes, que pueden incluir datos sobre tu actividad profesional, tu trayectoria y, si tú decides contarlo, aspectos personales. Las usamos únicamente para preparar la entrevista y el episodio, y se envían por correo electrónico al equipo del programa; la web no las guarda. No las cedemos a terceros. La base legal es tu consentimiento, que das al aceptar las <a href="terminos.html#participacion">condiciones de participación</a> y esta política, y puedes pedirnos que las borremos en cualquier momento.</p> <p><strong>Ficha del invitado.</strong> Si participas como invitado y rellenas la ficha (a la que se accede solo mediante un enlace personal), recogemos tu correo electrónico (para enviarte el recordatorio de la cita y la dirección del estudio), tu teléfono móvil (solo para contactarte el día de la grabación si surge algún imprevisto) y las respuestas que escribes, que pueden incluir datos sobre tu actividad profesional, tu trayectoria y, si tú decides contarlo, aspectos personales. Las usamos únicamente para preparar la entrevista y el episodio, y se envían por correo electrónico al equipo del programa; la web no las guarda. No las cedemos a terceros. La base legal es tu consentimiento, que das al aceptar las <a href="terminos.html#participacion">condiciones de participación</a> y esta política, y puedes pedirnos que las borremos en cualquier momento.</p>
<h2>4. Nuestra aplicación de publicación</h2> <h2>4. Nuestra aplicación de publicación</h2>
<p>Usamos una aplicación de gestión de redes sociales de código abierto (Postiz), instalada en un servidor propio, para programar y publicar el contenido del programa. La aplicación está pensada para uso interno: <strong>solo la utiliza el propietario de las cuentas</strong> y no está abierta al público.</p> <p>Usamos una aplicación de gestión de redes sociales de código abierto (Postiz), instalada en un servidor propio, para programar y publicar el contenido del programa. La aplicación está pensada para uso interno: <strong>solo la utiliza el propietario de las cuentas</strong> y no está abierta al público.</p>
+5 -2
View File
@@ -186,6 +186,9 @@ class H(BaseHTTPRequestHandler):
email = clean(d.get('email'), 200) email = clean(d.get('email'), 200)
if not email or not EMAIL_RE.match(email): if not email or not EMAIL_RE.match(email):
return self.reply(400, {'ok': False, 'error': 'El correo no parece válido.'}) return self.reply(400, {'ok': False, 'error': 'El correo no parece válido.'})
tel = ' '.join(str(d.get('tel') or '').split())
if not re.match(r'^\+?[\d ().-]{7,20}$', tel):
return self.reply(400, {'ok': False, 'error': 'El teléfono no parece válido.'})
lineas, seccion = [], None lineas, seccion = [], None
for q in GUEST_Q: for q in GUEST_Q:
ans = str(resp.get(q['id']) or '').replace('\r', '').strip()[:3000] ans = str(resp.get(q['id']) or '').replace('\r', '').strip()[:3000]
@@ -196,7 +199,7 @@ class H(BaseHTTPRequestHandler):
lineas += ['', f'== {seccion} =='] lineas += ['', f'== {seccion} ==']
lineas += ['', f"• {q['t']}", ' ' + (ans.replace('\n', '\n ') if ans else '(sin respuesta)')] lineas += ['', f"• {q['t']}", ' ' + (ans.replace('\n', '\n ') if ans else '(sin respuesta)')]
cabecera = [f'Ficha del invitado: {nombre}', 'Recibida: ' + time.strftime('%d/%m/%Y %H:%M', time.localtime()), cabecera = [f'Ficha del invitado: {nombre}', 'Recibida: ' + time.strftime('%d/%m/%Y %H:%M', time.localtime()),
f'Cita: {cita_txt(ts)}', f'Correo del invitado: {email}', f'Cita: {cita_txt(ts)}', f'Correo del invitado: {email}', f'Teléfono móvil: {tel}',
'Aceptó las condiciones de participación y la política de privacidad: sí'] 'Aceptó las condiciones de participación y la política de privacidad: sí']
try: try:
send(f'[Invitado] {clean(nombre, 80)} · ficha del podcast', email, '\n'.join(cabecera + lineas) + '\n') send(f'[Invitado] {clean(nombre, 80)} · ficha del podcast', email, '\n'.join(cabecera + lineas) + '\n')
@@ -211,7 +214,7 @@ class H(BaseHTTPRequestHandler):
try: try:
send('Tu cita en Anda Que No Sabes Ná', None, send('Tu cita en Anda Que No Sabes Ná', None,
f'Hola, {nombre}:\n\nGracias por rellenar tu ficha. Recuerda que grabamos el ' f'Hola, {nombre}:\n\nGracias por rellenar tu ficha. Recuerda que grabamos el '
f'{cita_txt(ts)}, y te enviaremos por correo la dirección del estudio.\n\n' f'{cita_txt(ts)}, y te enviaremos por correo electrónico la dirección del estudio.\n\n'
'Si no puedes venir o necesitas cambiar algo, responde a este correo.\n\n' 'Si no puedes venir o necesitas cambiar algo, responde a este correo.\n\n'
'Un abrazo,\nÁlex y Juan\nAnda Que No Sabes Ná\nhttps://andaquenosabesna.com\n', to=email) 'Un abrazo,\nÁlex y Juan\nAnda Que No Sabes Ná\nhttps://andaquenosabesna.com\n', to=email)
except Exception as exc: except Exception as exc: