chore(kb): note noi (youtube, facebook, fișe grup sprijin, email) + tool scout_voluntar_check

Reindexare KB, timestamps cron/habits actualizate din rulările automate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 08:53:49 +00:00
parent 1f0ed3ca12
commit 553a137206
21 changed files with 1047 additions and 83 deletions

View File

@@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""
Verifica emailuri noi de inscriere cercetasi bleuMarin Constanta (voluntari +
lista de asteptare copii) si formateaza un mesaj WhatsApp gata de trimis
pentru colegul care se ocupa de voluntari noi.
Nu salveaza nimic in memory/kb/ - doar citeste emailurile nou-sosite (unread),
extrage campurile si le marcheaza ca citite.
Usage:
python3 tools/scout_voluntar_check.py
"""
import imaplib
import email
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from tools.email_process import (
IMAP_SERVER, IMAP_PORT, IMAP_USER, IMAP_PASS, WHITELIST,
decode_mime_header, get_email_body, extract_sender_email,
)
# Ordinea exacta a campurilor asa cum apar in notificarea Gravity Forms
# (text simplu, fara separatori intre eticheta si valoare) pentru fiecare
# formular de pe scout-bleumarin.ro. Daca site-ul schimba formularul,
# aceste liste trebuie actualizate.
LABELS_VOLUNTAR = [
"Email",
"Nume și prenume",
"Vârsta (peste 18 ani)",
"Telefon",
"Câte ore pe lună ești dispus să investești pentru activitățile de cercetași?",
"Skill profesional",
"De unde ai aflat de cercetași?",
"Care este motivația ta de a te înscrie ca voluntar adult?",
"Avem beneficiari copii și tineri din fiecare ciclu de școală (clasele 1-4, clasele 5-8 si liceu), unde consideri că ai putea ajuta?",
"În ce categorie de activități crezi că ai putea aduce un plus de valoare?",
"Care sunt primele 3 valori morale ce te vor însoți în drumul tău alături de cercetași?",
"Care sunt valorile (principile) ce te aștepți să le găsești la ceilalți voluntari?",
]
LABELS_COPIL = [
"Nume și prenume copil",
"Nume și prenume părinte",
"Număr telefon părinte",
"Adresă e-mail părinte",
"În ce clasă va fi la școală în luna octombrie? (începutul anului cercetășesc următor)",
"Data nașterii",
"Care este motivația înscrierii la cercetași?",
"Copilul are frați, colegi de școală sau prieteni la cercetași? Dacă da, completați numele.",
"De unde ați aflat de activitatea noastră?",
]
def extract_fields(body: str, labels: list[str]) -> dict:
"""Extrage valorile dintre etichete consecutive dintr-un text Gravity Forms.
Emailul e text simplu cu line-wrap la ~70 caractere, care poate rupe o
eticheta la mijloc (ex. "Vârsta (peste 18\\nani)") - normalizam orice
secventa de whitespace la un singur spatiu inainte de a cauta etichetele.
"""
body = re.sub(r"\s+", " ", body)
pattern = "|".join(re.escape(l) for l in labels)
matches = list(re.finditer(pattern, body))
fields = {}
for i, m in enumerate(matches):
label = m.group(0)
start = m.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(body)
fields[label] = body[start:end].strip(" \n\r\t:")
return fields
def extract_id(body: str) -> str:
m = re.search(r"Răspuns #(\d+)", body)
return m.group(1) if m else "?"
def digits_only(s: str) -> str:
return re.sub(r"\D", "", s or "")
def parse_email(body: str, subject: str) -> dict | None:
body = re.sub(r"\s+", " ", body)
subj = subject.lower()
if "voluntari" in subj:
f = extract_fields(body, LABELS_VOLUNTAR)
return {
"tip": "voluntar",
"id": extract_id(body),
"nume": f.get("Nume și prenume", "?"),
"varsta": f.get("Vârsta (peste 18 ani)", "?"),
"telefon": f.get("Telefon", "?"),
"email": f.get("Email", "?"),
"disponibilitate": f.get("Câte ore pe lună ești dispus să investești pentru activitățile de cercetași?", "?"),
"skill": f.get("Skill profesional", "?"),
"sursa": f.get("De unde ai aflat de cercetași?", "?"),
"interes_varsta": f.get("Avem beneficiari copii și tineri din fiecare ciclu de școală (clasele 1-4, clasele 5-8 si liceu), unde consideri că ai putea ajuta?", "?"),
"interes_activitati": f.get("În ce categorie de activități crezi că ai putea aduce un plus de valoare?", "?"),
}
if "așteptare" in subj or "asteptare" in subj:
f = extract_fields(body, LABELS_COPIL)
return {
"tip": "copil",
"id": extract_id(body),
"nume_copil": f.get("Nume și prenume copil", "?"),
"nume_parinte": f.get("Nume și prenume părinte", "?"),
"telefon": f.get("Număr telefon părinte", "?"),
"email": f.get("Adresă e-mail părinte", "?"),
"clasa": f.get("În ce clasă va fi la școală în luna octombrie? (începutul anului cercetășesc următor)", "?"),
"data_nasterii": f.get("Data nașterii", "?"),
"sursa": f.get("De unde ați aflat de activitatea noastră?", "?"),
}
return None
def fetch_unseen_scout_emails() -> list[dict]:
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
mail.login(IMAP_USER, IMAP_PASS)
mail.select("INBOX")
status, messages = mail.search(None, "UNSEEN")
email_ids = messages[0].split() if messages[0] else []
records = []
seen_ids = []
for eid in email_ids:
status, data = mail.fetch(eid, "(BODY.PEEK[])")
if status != "OK":
continue
msg = email.message_from_bytes(data[0][1])
from_addr = decode_mime_header(msg["From"])
sender_email = extract_sender_email(from_addr)
subject = decode_mime_header(msg["Subject"])
body = get_email_body(msg)
is_forward = "fwd:" in subject.lower()
if sender_email not in WHITELIST and not is_forward:
continue
if "scout-bleumarin.ro" not in body:
continue
parsed = parse_email(body, subject)
if parsed:
records.append(parsed)
seen_ids.append(eid)
# Marcheaza doar emailurile procesate ca citite
for eid in seen_ids:
mail.store(eid, "+FLAGS", "\\Seen")
mail.logout()
return records
def format_message(records: list[dict]) -> str:
if not records:
return "Niciun email nou de inscriere cercetasi."
voluntari = [r for r in records if r["tip"] == "voluntar"]
copii = [r for r in records if r["tip"] == "copil"]
blocks = []
for v in voluntari:
copii_asociati = [c for c in copii if digits_only(c["telefon"]) == digits_only(v["telefon"])]
lines = [
f"Voluntar nou inscris - bleuMarin Constanta (ID #{v['id']})",
"",
f"Nume: {v['nume']}, {v['varsta']} ani",
f"Tel: {v['telefon']}",
f"Email: {v['email']}",
"",
f"Disponibilitate: {v['disponibilitate']}",
f"Skill: {v['skill']}",
f"Interes: {v['interes_varsta']} - {v['interes_activitati']}",
f"De unde a aflat: {v['sursa']}",
]
for c in copii_asociati:
lines.append("")
lines.append(
f"Notă: are și un copil pe lista de așteptare (ID #{c['id']}) - "
f"{c['nume_copil']} (n. {c['data_nasterii']}, clasa din octombrie: {c['clasa']}). "
f"De unde a aflat: {c['sursa']}"
)
blocks.append("\n".join(lines))
copii = [c for c in copii if c not in copii_asociati]
for c in copii:
blocks.append(
"\n".join([
f"Copil nou pe lista de asteptare - bleuMarin Constanta (ID #{c['id']})",
"",
f"Copil: {c['nume_copil']} (n. {c['data_nasterii']})",
f"Parinte: {c['nume_parinte']}",
f"Tel: {c['telefon']}",
f"Email: {c['email']}",
f"Clasa din octombrie: {c['clasa']}",
f"De unde a aflat: {c['sursa']}",
])
)
return "\n\n---\n\n".join(blocks)
if __name__ == "__main__":
records = fetch_unseen_scout_emails()
print(format_message(records))

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Self-check pentru parsarea emailurilor de inscriere cercetasi.
Ruleaza: python3 tools/test_scout_voluntar_check.py
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from tools.scout_voluntar_check import parse_email
BODY_VOLUNTAR = """
Organizația Natională Cercetașii României - Centrul Local bleuMarin
Constanța <https://scout-bleumarin.ro/ro>
Formularul "Înscriere voluntari bleuMarin Constanța" a primit un nou
răspuns.
Răspunsul a fost stocat și în baza de date cu următorul id:
Răspuns #516 <https://scout-bleumarin.ro/admin/form_submission/516> Email
ureche.irina94@yahoo.com Nume și prenume Purcărea Irina Vârsta (peste 18
ani) 32 Telefon 0729127010 Câte ore pe lună ești dispus să investești
pentru activitățile de cercetași? 12-15 (3 ore pe săptămână) - minim pentru
lider full-time Skill profesional Experiență în antreprenoriat, organizarea
și gestionarea unei afaceri și comunicare. De unde ai aflat de cercetași? Un
eveniment în complex Tom. Care este motivația ta de a te înscrie ca
voluntar adult? Îmi doresc să mă înscriu deoarece vreau să contribui la
dezvoltarea copiilor. Avem beneficiari copii și tineri din fiecare ciclu de școală
(clasele 1-4, clasele 5-8 si liceu), unde consideri că ai putea ajuta? clasele
1-4 (lupisori) În ce categorie de activități crezi că ai putea aduce un
plus de valoare? Comunicare și organizare evenimente. Care sunt primele 3 valori morale ce te vor
însoți în drumul tău alături de cercetași? Respectul, responsabilitatea și
empatia. Care sunt valorile (principile) ce te aștepți să le găsești la
ceilalți voluntari? Respect și deschidere.
"""
BODY_COPIL = """
Formularul "Listă așteptare" a primit un nou răspuns.
Răspunsul a fost stocat și în baza de date cu următorul id:
Răspuns #517 <https://scout-bleumarin.ro/admin/form_submission/517> Nume și
prenume copil Purcărea Amelia Ioana Nume și prenume părinte Purcărea
Irina Număr
telefon părinte 0729127010 Adresă e-mail părinte ureche.irina94@yahoo.com În
ce clasă va fi la școală în luna octombrie? (începutul anului cercetășesc
următor) clasa I Data nașterii 2018-10-10 Care este motivația înscrierii la
cercetași? Îmi doresc. Copilul are frați, colegi de școală sau prieteni la
cercetași? Dacă da, completați numele. Nu din câte știm. De unde ați aflat de
activitatea noastră? O activitate din centrul comercial Tom.
"""
def demo():
v = parse_email(BODY_VOLUNTAR, "Fwd: Înscriere voluntari bleuMarin Constanța")
assert v is not None
assert v["id"] == "516"
assert v["nume"] == "Purcărea Irina"
assert v["telefon"] == "0729127010"
assert v["sursa"] == "Un eveniment în complex Tom."
c = parse_email(BODY_COPIL, "Fwd: Listă așteptare")
assert c is not None
assert c["id"] == "517"
assert c["nume_copil"] == "Purcărea Amelia Ioana"
assert c["telefon"] == "0729127010"
assert c["sursa"] == "O activitate din centrul comercial Tom."
print("OK - parsare voluntar + copil corecta")
if __name__ == "__main__":
demo()