"""Routines: terugkerende taken van de CRM (e-mail ophalen, nieuwsbrieven versturen, …). Een routinetype is een functie met @register; een routine (tabel jobs) is een ingestelde instantie van een type met een interval en eigen instellingen (config, JSON). Er kunnen meerdere routines van hetzelfde type zijn, bijv. twee keer e-mail ophalen met andere mailboxen en een ander interval. Uitvoeren: `python app.py scheduler` (blijft draaien, kijkt elke 30 s; de Docker-container crm-routines) of `python app.py tick` (één keer, voor cron elke minuut). Een routine draait nooit twee keer tegelijk (running_since werkt als slot; na STALE_MINUTES geldt een slot als achtergebleven na een crash). Een nieuw type toevoegen, in app.py: @jobs.register('rapport', 'Weekrapport', 'Mailt elke week de nieuwe leads.', interval=7 * 24 * 60, fields=[jobs.Field('to', 'Naar', 'email')]) def job_rapport(job, config): ... # gewone app-context: db(), q(), notify(), mail… return '12 leads gemeld' # komt in het logboek; een exception = status fout Een standaardroutine die er altijd moet zijn, krijgt `default=True`: die maakt app.py bij het starten één keer aan (actief, met het standaardinterval). """ from dataclasses import dataclass, field STALE_MINUTES = 60 KEEP_RUNS = 200 # logregels per routine UNITS = [(1, 'minuten'), (60, 'uur'), (24 * 60, 'dagen')] @dataclass class Field: """Instelling van een routinetype. kind: text, number, email, bool, mailboxes (meerkeuze uit de mailboxen).""" key: str label: str kind: str = 'text' default: object = '' hint: str = '' @dataclass class JobType: key: str label: str description: str run: object interval: int = 60 # standaardinterval in minuten fields: list = field(default_factory=list) default: bool = False # bij het starten één keer aanmaken TYPES = {} def register(key, label, description='', interval=60, fields=(), default=False): def wrap(fn): TYPES[key] = JobType(key, label, description, fn, interval, list(fields), default) return fn return wrap def defaults(kind): return {f.key: f.default for f in TYPES[kind].fields} def clean_config(kind, form): """Instellingen uit een formulier (request.form) volgens de velden van het type.""" out = {} for f in TYPES[kind].fields: if f.kind == 'bool': out[f.key] = bool(form.get(f.key)) elif f.kind == 'number': try: out[f.key] = int(form.get(f.key) or f.default or 0) except ValueError: out[f.key] = f.default elif f.kind == 'mailboxes': out[f.key] = [int(v) for v in form.getlist(f.key) if v.isdigit()] else: out[f.key] = (form.get(f.key) or '').strip() return out def interval_text(minutes): for size, unit in reversed(UNITS): if minutes % size == 0 and minutes >= size: n = minutes // size return {'minuten': f'{n} min', 'uur': f'{n} uur', 'dagen': f'{n} dag' + ('en' if n != 1 else '')}[unit] return f'{minutes} min'