build: invites from the plan, and rebuilding a live site without losing its words
`invites_by` on a plan (and `invited_by` on a group) put an invite form per fillable group on the inviting group's desk, plus a list of the invites sent - portal's `invite:` does the onboarding. Every desk group invites into itself by default; an inviter without a desk is refused. `--words-from <repo>` lifts the words an existing repo carries back into the words map by structure: the plan is rendered with a token in every slot and walked alongside the repo's files. Hand edits and model-chosen icons survive a rebuild; only new slots get fallbacks, which `--words <json>` can override. The words as used are written to words.json. First use: Tomter Vel, additions only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
255aeb4f4b
commit
93a6fc0e10
@@ -32,6 +32,23 @@ The model writes words only, one page at a time, into a JSON object
|
||||
whose shape is fixed. Its strings are merged over built-in fallbacks
|
||||
key by key, so a bad reply costs wording, never a working site.
|
||||
|
||||
### Onboarding from the plan
|
||||
|
||||
`invites_by: [styret]` on a plan, or `invited_by: [...]` on a group,
|
||||
puts an invite form on the inviting group's desk for each group it may
|
||||
fill, and a list of the invites sent. Portal does the rest (an account
|
||||
in Kanidm, the group, a one-time credential link) through `invite:` on
|
||||
the alternative. Every group with a desk invites into itself by
|
||||
default.
|
||||
|
||||
### Rebuilding a live site
|
||||
|
||||
`--words-from <repo>` lifts the words a repo already has back into the
|
||||
words map by structure, so a rebuild keeps hand-edited wording and
|
||||
adds only the new slots; `--words <json>` overrides slots by page;
|
||||
`words.json` in the output is the words as used. See
|
||||
`runs/tomtervel-invites-2026-09-23`.
|
||||
|
||||
| Same contractor, same local model (nemotron 30B-A3B on ergo) | Free-form `generate` | `build` |
|
||||
|---|---|---|
|
||||
| A site the verifier accepts | never, in 6 rounds and 16 min | first try, 130 s |
|
||||
|
||||
@@ -24,6 +24,10 @@ groups:
|
||||
- { id: pendlerforhold, label: Pendlerforhold }
|
||||
- { id: trafikk, label: Trafikkforhold }
|
||||
|
||||
# Styret kan invitere inn i alle grupper som har en pult; hver gruppe kan
|
||||
# invitere inn i seg selv. Portal oppretter kontoen og en engangslenke.
|
||||
invites_by: [styret]
|
||||
|
||||
doors:
|
||||
- id: medlem
|
||||
who: en husstand i Tomter som vil bli medlem, betale kontingent, eller melde flytting, ny e-post eller utmelding
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Tomter Vel: invites on every desk, 2026-09-23
|
||||
|
||||
The first rebuild of a live site from its plan with `--words-from`:
|
||||
every word the repo carried (hand-edited Norwegian, model-chosen
|
||||
icons) lifted back into the words map by structure, the new invite
|
||||
slots given Norwegian by `invite-words-no.json`, and the result
|
||||
diffed against the live repo: additions only, 33 lines per committee
|
||||
desk and 145 on styret's, which invites into all eight groups.
|
||||
|
||||
`words.json` is the words as used; the next rebuild can start from it
|
||||
with `--words words.json` instead of lifting.
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"questions/arrangementer/index.yaml": { "invites": {
|
||||
"arrangementer": { "heading": "Inviter noen inn i Arrangementer", "description": "De får en konto, plass i Arrangementer, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } }
|
||||
}, "invited": { "heading": "Sendte invitasjoner", "description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.", "done": "Er inne" } }
|
||||
,
|
||||
"questions/horingsinstans/index.yaml": { "invites": {
|
||||
"horingsinstans": { "heading": "Inviter noen inn i Høringsinstans", "description": "De får en konto, plass i Høringsinstans, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } }
|
||||
}, "invited": { "heading": "Sendte invitasjoner", "description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.", "done": "Er inne" } }
|
||||
,
|
||||
"questions/kasserer/index.yaml": { "invites": {
|
||||
"kasserer": { "heading": "Inviter noen inn i kassereren", "description": "De får en konto, plass i kassereren, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } }
|
||||
}, "invited": { "heading": "Sendte invitasjoner", "description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.", "done": "Er inne" } }
|
||||
,
|
||||
"questions/komiteer/index.yaml": { "invites": {
|
||||
"komiteer": { "heading": "Inviter noen inn i komiteene", "description": "De får en konto, plass i komiteene, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } }
|
||||
}, "invited": { "heading": "Sendte invitasjoner", "description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.", "done": "Er inne" } }
|
||||
,
|
||||
"questions/lekeplasser/index.yaml": { "invites": {
|
||||
"lekeplasser": { "heading": "Inviter noen inn i Leke- og møteplasser", "description": "De får en konto, plass i Leke- og møteplasser, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } }
|
||||
}, "invited": { "heading": "Sendte invitasjoner", "description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.", "done": "Er inne" } }
|
||||
,
|
||||
"questions/nabohjelp/index.yaml": { "invites": {
|
||||
"nabohjelp": { "heading": "Inviter noen inn i nabohjelpen", "description": "De får en konto, plass i nabohjelpen, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } }
|
||||
}, "invited": { "heading": "Sendte invitasjoner", "description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.", "done": "Er inne" } }
|
||||
,
|
||||
"questions/styret/index.yaml": { "invites": {
|
||||
"arrangementer": { "heading": "Inviter noen inn i Arrangementer", "description": "De får en konto, plass i Arrangementer, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } },
|
||||
"horingsinstans": { "heading": "Inviter noen inn i Høringsinstans", "description": "De får en konto, plass i Høringsinstans, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } },
|
||||
"kasserer": { "heading": "Inviter noen inn i kassereren", "description": "De får en konto, plass i kassereren, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } },
|
||||
"komiteer": { "heading": "Inviter noen inn i komiteene", "description": "De får en konto, plass i komiteene, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } },
|
||||
"lekeplasser": { "heading": "Inviter noen inn i Leke- og møteplasser", "description": "De får en konto, plass i Leke- og møteplasser, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } },
|
||||
"nabohjelp": { "heading": "Inviter noen inn i nabohjelpen", "description": "De får en konto, plass i nabohjelpen, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } },
|
||||
"styret": { "heading": "Inviter noen inn i styret", "description": "De får en konto, plass i styret, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } },
|
||||
"trafikk": { "heading": "Inviter noen inn i Trafikkforhold", "description": "De får en konto, plass i Trafikkforhold, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } }
|
||||
}, "invited": { "heading": "Sendte invitasjoner", "description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.", "done": "Er inne" } }
|
||||
,
|
||||
"questions/trafikk/index.yaml": { "invites": {
|
||||
"trafikk": { "heading": "Inviter noen inn i Trafikkforhold", "description": "De får en konto, plass i Trafikkforhold, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.", "button": "Inviter", "form_title": "Hvem", "labels": { "name": "Navn", "email": "E-post" } }
|
||||
}, "invited": { "heading": "Sendte invitasjoner", "description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.", "done": "Er inne" } }
|
||||
}
|
||||
@@ -0,0 +1,873 @@
|
||||
{
|
||||
"questions/arrangementer/index.yaml": {
|
||||
"buckets": {
|
||||
"frivillige": {
|
||||
"description": "Her ser du hvem som har meldt seg til dugnad, 17. mai eller andre arrangementer, og hvor langt du har kommet med hver av dem.",
|
||||
"heading": "Frivillige til arrangementer og dugnad",
|
||||
"moves": {
|
||||
"kontaktet->satt_opp": "Sett opp",
|
||||
"kontaktet->takket_nei": "Takket nei",
|
||||
"open->kontaktet": "Merk som kontaktet",
|
||||
"open->takket_nei": "Takket nei"
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirm": "Lagre",
|
||||
"description": "Oversikt over folk som vil hjelpe til, slik at du kan følge opp og sette dem opp på noe som passer.",
|
||||
"invited": {
|
||||
"description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.",
|
||||
"done": "Er inne",
|
||||
"heading": "Sendte invitasjoner"
|
||||
},
|
||||
"invites": {
|
||||
"arrangementer": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i Arrangementer, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i Arrangementer",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {},
|
||||
"question": "Hvem har meldt seg, og hvor langt har jeg kommet med å sette dem opp?"
|
||||
},
|
||||
"questions/bidra/index.yaml": {
|
||||
"description": "Vil du stille opp for nabolaget? Her melder du deg til dugnad og arrangementer, eller til en av komiteene.",
|
||||
"needs": {
|
||||
"hjelpe-til": {
|
||||
"button": "Meld meg på",
|
||||
"description": "17. mai, dugnad eller andre arrangementer trenger folk som stiller opp. Si fra hva du kan bidra med, så setter vi deg opp på noe som passer.",
|
||||
"encouragement": "Det holder med noen ord om hva du kan tenke deg – vi tar kontakt når det nærmer seg.",
|
||||
"form_description": "Fortell kort hva du kan hjelpe til med.",
|
||||
"form_title": "Meld deg som hjelper",
|
||||
"heading": "Bli med på dugnad eller arrangement",
|
||||
"hints": {
|
||||
"hva": "",
|
||||
"navn": "Kari Nordmann",
|
||||
"telefon": "912 34 567"
|
||||
},
|
||||
"icon": "hand-helping",
|
||||
"labels": {
|
||||
"hva": "Hva kan du hjelpe til med?",
|
||||
"navn": "Navn",
|
||||
"telefon": "Telefon"
|
||||
},
|
||||
"road_description": "Arrangementskomiteen tar kontakt og finner ut hva som passer for deg. Sier du ja, blir du satt opp på en oppgave – sier du nei takk, er det helt greit.",
|
||||
"road_icon": "lucide:clipboard-list",
|
||||
"road_title": "Hva skjer videre",
|
||||
"who_description": "Arrangementskomiteen leser det du sender, og du kan nå dem på post@tomtervel.no i mellomtiden.",
|
||||
"who_icon": "lucide:users",
|
||||
"who_title": "Hvem leser dette"
|
||||
},
|
||||
"komite": {
|
||||
"button": "Meld interesse",
|
||||
"description": "Velet drives av sju komiteer på fritida, og alle trenger flere hender. Si hvilken komité du har lyst til å være med i.",
|
||||
"encouragement": "Det tar bare et minutt, og noen fra styret svarer deg.",
|
||||
"form_description": "Skriv hvilken komité du er interessert i.",
|
||||
"form_title": "Meld deg til en komité",
|
||||
"heading": "Sitt i en komité som betyr noe for deg",
|
||||
"hints": {
|
||||
"epost": "kari@eksempel.no",
|
||||
"komite": "",
|
||||
"navn": "Kari Nordmann"
|
||||
},
|
||||
"icon": "users",
|
||||
"labels": {
|
||||
"epost": "E-post",
|
||||
"komite": "Hvilken komité?",
|
||||
"navn": "Navn"
|
||||
},
|
||||
"road_description": "Styret ser på ønsket ditt og finner en plass i komiteen du har meldt deg til, eller sier fra hvis det ikke er rom akkurat nå.",
|
||||
"road_icon": "lucide:clipboard-list",
|
||||
"road_title": "Hva skjer videre",
|
||||
"who_description": "Styret leser det du sender, og du kan nå dem på post@tomtervel.no i mellomtiden.",
|
||||
"who_icon": "lucide:handshake",
|
||||
"who_title": "Hvem leser dette"
|
||||
}
|
||||
},
|
||||
"question": "Vil du hjelpe til i nabolaget?"
|
||||
},
|
||||
"questions/decide/dele-utstyr.yaml": {
|
||||
"description": "Her sier du fra at utstyret er hentet eller ikke lenger tilgjengelig for utlån.",
|
||||
"moves": {
|
||||
"tatt_ned": {
|
||||
"button": "Ta ned oppslaget",
|
||||
"description": "Oppslaget fjernes fra siden, så ingen flere spør om å låne det.",
|
||||
"heading": "Meld at utstyret er tatt ned"
|
||||
}
|
||||
},
|
||||
"question": "Skal oppslaget om utstyret fortsatt stå ute?"
|
||||
},
|
||||
"questions/decide/kan-hjelpe.yaml": {
|
||||
"description": "Her ser du naboer som har bedt om en hånd, og kan velge om du fortsatt kan bidra eller trekke deg for nå.",
|
||||
"moves": {
|
||||
"trukket": {
|
||||
"button": "Trekk meg",
|
||||
"description": "Du blir tatt av lista over naboer som kan spørres, men kan melde deg på igjen når det passer bedre.",
|
||||
"heading": "Ta en pause fra nabohjelpen"
|
||||
}
|
||||
},
|
||||
"question": "Vil du fortsatt kunne bli spurt om å hjelpe en nabo?"
|
||||
},
|
||||
"questions/decide/trenger-hjelp.yaml": {
|
||||
"description": "Her ser du henvendelsen fra naboen som trengte en hånd, og kan si fra når det er ordnet.",
|
||||
"moves": {
|
||||
"ordnet": {
|
||||
"button": "Merk som ordnet",
|
||||
"description": "Da vet vi at naboen har fått hjelpen sin, og saken lukkes.",
|
||||
"heading": "Meld fra at hjelpen er gitt"
|
||||
}
|
||||
},
|
||||
"question": "Har naboen fått hjelpen de trengte?"
|
||||
},
|
||||
"questions/horingsinstans/index.yaml": {
|
||||
"buckets": {
|
||||
"innspill": {
|
||||
"description": "Her ligger innspill fra beboere til saker kommunen har på høring.",
|
||||
"heading": "Innspill til høringssvar",
|
||||
"moves": {
|
||||
"open->ikke_tatt_med": "Ikke tatt med",
|
||||
"open->tatt_med": "Tatt med i svaret"
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirm": "Send svar",
|
||||
"description": "Her samler du innspillene og ser hva som er tatt med i velets høringssvar.",
|
||||
"invited": {
|
||||
"description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.",
|
||||
"done": "Er inne",
|
||||
"heading": "Sendte invitasjoner"
|
||||
},
|
||||
"invites": {
|
||||
"horingsinstans": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i Høringsinstans, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i Høringsinstans",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {},
|
||||
"question": "Hva sier vi i høringssvaret?"
|
||||
},
|
||||
"questions/index.yaml": {
|
||||
"description": "Tomter Vel tar vare på nærmiljøet – fra elvestien og lekeplassene til trafikksikkerhet og naboskap. Finn det som gjelder deg.",
|
||||
"doors": {
|
||||
"bidra": {
|
||||
"button": "Bli med",
|
||||
"description": "Vi trenger folk på 17. mai, dugnader og i komiteene. Det trengs ikke mer enn litt tid du har å avse.",
|
||||
"for_whom": "Deg som vil gjøre en innsats",
|
||||
"heading": "Bli med og gjør en innsats",
|
||||
"icon": "hand-helping"
|
||||
},
|
||||
"medlem": {
|
||||
"button": "Til medlemskap",
|
||||
"description": "Meld deg inn, betal kontingenten, eller gi beskjed om flytting, ny e-post eller utmelding.",
|
||||
"for_whom": "Deg som bor i Tomter",
|
||||
"heading": "Bli medlem eller endre opplysningene dine",
|
||||
"icon": "house"
|
||||
},
|
||||
"melde-fra": {
|
||||
"button": "Meld fra",
|
||||
"description": "Sett du noe farlig i trafikken, eller noe ødelagt på lekeplassen eller elvestien? Gi oss beskjed.",
|
||||
"for_whom": "Deg som har sett noe som bør fikses",
|
||||
"heading": "Meld fra om noe som bør fikses",
|
||||
"icon": "alert-triangle"
|
||||
},
|
||||
"naboer": {
|
||||
"button": "Til nabohjelp",
|
||||
"description": "Trenger du en hånd med snømåking, handling eller skyss? Eller kan du stille opp for en nabo? Her møtes dere.",
|
||||
"for_whom": "Deg som trenger hjelp eller kan gi en hånd",
|
||||
"heading": "Få hjelp av naboer, eller gi en hånd selv",
|
||||
"icon": "heart-handshake"
|
||||
}
|
||||
},
|
||||
"needs": {
|
||||
"horingsinnspill": {
|
||||
"button": "Send innspill",
|
||||
"description": "Har kommunen en sak på høring du mener noe om? Send oss innspillet ditt, så tar vi det med i velets svar.",
|
||||
"encouragement": "Innspillet ditt blir lest av høringsutvalget vårt og veier inn i svaret vi sender kommunen.",
|
||||
"form_description": "Fortell hvilken sak det gjelder og hva du mener.",
|
||||
"form_title": "Send et høringsinnspill",
|
||||
"heading": "Få din mening med i høringssvaret",
|
||||
"hints": {
|
||||
"innspill": "Jeg mener det bør settes opp fartsdumper langs den nye traseen, for barna krysser der til skolen.",
|
||||
"navn": "Kari Nordmann",
|
||||
"sak": "Reguleringsplan for Tomterveien"
|
||||
},
|
||||
"icon": "megaphone",
|
||||
"labels": {
|
||||
"innspill": "Hva mener du?",
|
||||
"navn": "Navn",
|
||||
"sak": "Hvilken sak gjelder det?"
|
||||
},
|
||||
"road_description": "Innspillet ditt kommer inn hos høringsutvalget. De vurderer om det tas med i velets svar til kommunen, eller om det ikke passer inn denne gangen.",
|
||||
"road_icon": "lucide:route",
|
||||
"road_title": "Hva skjer videre",
|
||||
"who_description": "Høringsutvalget i Tomter Vel leser innspillet ditt; i mellomtiden kan du nå styret på post@tomtervel.no.",
|
||||
"who_icon": "lucide:users",
|
||||
"who_title": "Hvem leser dette"
|
||||
}
|
||||
},
|
||||
"question": "Hva gjelder det for deg?"
|
||||
},
|
||||
"questions/kasserer/index.yaml": {
|
||||
"buckets": {
|
||||
"husstander": {
|
||||
"description": "Husstander som har meldt seg inn og venter på å bli registrert som betalende medlem.",
|
||||
"heading": "Nye medlemmer som venter på registrering",
|
||||
"moves": {
|
||||
"open->betalt": "Registrer betaling",
|
||||
"open->utmeldt": "Meld ut"
|
||||
}
|
||||
},
|
||||
"medlemsendringer": {
|
||||
"description": "Medlemmer som har flyttet, byttet e-post eller vil melde seg ut, og venter på at opplysningene blir oppdatert.",
|
||||
"heading": "Endringer som venter på å bli registrert",
|
||||
"moves": {
|
||||
"open->oppdatert": "Registrer endring"
|
||||
}
|
||||
},
|
||||
"utlegg": {
|
||||
"description": "Utlegg fra komitémedlemmer som venter på godkjenning eller utbetaling.",
|
||||
"heading": "Utlegg som venter på deg",
|
||||
"moves": {
|
||||
"godkjent->avvist": "Avvis likevel",
|
||||
"godkjent->utbetalt": "Merk som utbetalt",
|
||||
"open->avvist": "Avvis utlegg",
|
||||
"open->godkjent": "Godkjenn utlegg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirm": "Lagre",
|
||||
"description": "Her har du oversikt over medlemmer, endringer og utlegg som venter på deg som kasserer.",
|
||||
"invited": {
|
||||
"description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.",
|
||||
"done": "Er inne",
|
||||
"heading": "Sendte invitasjoner"
|
||||
},
|
||||
"invites": {
|
||||
"kasserer": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i kassereren, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i kassereren",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {},
|
||||
"question": "Hva venter på meg som kasserer akkurat nå?"
|
||||
},
|
||||
"questions/komiteer/index.yaml": {
|
||||
"buckets": {},
|
||||
"confirm": "Confirm",
|
||||
"description": "Her holder du oversikt over sakene komiteen din har ansvar for, og flytter dem videre etter hvert som de blir fulgt opp.",
|
||||
"invited": {
|
||||
"description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.",
|
||||
"done": "Er inne",
|
||||
"heading": "Sendte invitasjoner"
|
||||
},
|
||||
"invites": {
|
||||
"komiteer": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i komiteene, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i komiteene",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"utlegg": {
|
||||
"button": "Send utlegg",
|
||||
"description": "Har du lagt ut for noe til komiteen? Send det inn så kasserer kan betale deg tilbake.",
|
||||
"heading": "Få tilbake det du har lagt ut"
|
||||
}
|
||||
},
|
||||
"question": "Hva står ubehandlet i komiteen din nå?"
|
||||
},
|
||||
"questions/komiteer/utlegg-sent.yaml": {
|
||||
"back": "Til forsiden",
|
||||
"description": "Utlegget er sendt til kasserer. Du får beskjed når det er sjekket.",
|
||||
"next_description": "Kasserer ser gjennom kvitteringen og overfører pengene til kontoen du oppga.",
|
||||
"next_heading": "Pengene kommer på konto",
|
||||
"question": "Hva skjer med utlegget mitt nå?"
|
||||
},
|
||||
"questions/komiteer/utlegg.yaml": {
|
||||
"button": "Send inn",
|
||||
"description": "Har du lagt ut for noe til komiteen? Send det inn, så ordner kasserer resten.",
|
||||
"encouragement": "Legg ved bilag om du har det, så går det raskere.",
|
||||
"form_description": "Ha kvittering eller bilag klart før du fyller ut.",
|
||||
"form_title": "Krav om refusjon",
|
||||
"heading": "Få pengene tilbake",
|
||||
"hints": {
|
||||
"belop": "450 kr",
|
||||
"hva": "Kaffe og kjeks til dugnaden",
|
||||
"navn": "Kari Nordmann"
|
||||
},
|
||||
"icon": "receipt",
|
||||
"labels": {
|
||||
"belop": "Beløp",
|
||||
"hva": "Hva gjaldt det",
|
||||
"navn": "Navn"
|
||||
},
|
||||
"page_description": "Har du betalt for noe på vegne av en komité? Send inn utlegget, så får du pengene tilbake.",
|
||||
"question": "Har du lagt ut penger for komiteen og vil ha dem tilbake?",
|
||||
"road_description": "Kasserer ser gjennom kravet og godkjenner det, og deretter blir pengene utbetalt til deg. Blir det avvist, hører du hvorfor.",
|
||||
"road_icon": "lucide:wallet",
|
||||
"road_title": "Fra krav til utbetaling",
|
||||
"who_description": "Kasserer i styret leser kravet ditt, og kan nås på post@tomtervel.no om det haster.",
|
||||
"who_icon": "lucide:coins",
|
||||
"who_title": "Kasserer ser dette"
|
||||
},
|
||||
"questions/lekeplasser/index.yaml": {
|
||||
"buckets": {
|
||||
"anleggsfeil": {
|
||||
"description": "Meldinger om noe som er ødelagt på lekeplassen eller langs stien, og som bør fikses.",
|
||||
"heading": "Det som er meldt inn av skader",
|
||||
"moves": {
|
||||
"open->tatt_tak_i": "Ta tak i saken",
|
||||
"tatt_tak_i->utbedret": "Merk som utbedret"
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirm": "Lagre",
|
||||
"description": "Her følger komiteen for lekeplassene opp det som er meldt inn av skader, til det er fikset.",
|
||||
"invited": {
|
||||
"description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.",
|
||||
"done": "Er inne",
|
||||
"heading": "Sendte invitasjoner"
|
||||
},
|
||||
"invites": {
|
||||
"lekeplasser": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i Leke- og møteplasser, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i Leke- og møteplasser",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {},
|
||||
"question": "Hva må fikses på lekeplassene og stien nå?"
|
||||
},
|
||||
"questions/medlem/index.yaml": {
|
||||
"description": "Bli medlem av Tomter Vel, eller oppdater opplysningene dine hvis du allerede er medlem.",
|
||||
"needs": {
|
||||
"bli-medlem": {
|
||||
"button": "Bli medlem",
|
||||
"description": "Meld deg inn i velet, så holder vi deg oppdatert og du støtter arbeidet i nærmiljøet.",
|
||||
"encouragement": "Kasserer sender deg kontingenten på e-post rett etterpå.",
|
||||
"form_description": "Ha adressen i Tomter og en e-post du bruker klar.",
|
||||
"form_title": "Meld deg inn",
|
||||
"heading": "Bli medlem av Tomter Vel",
|
||||
"hints": {
|
||||
"adresse": "Tomterveien 12",
|
||||
"epost": "ola.nordmann@epost.no",
|
||||
"navn": "Ola Nordmann",
|
||||
"telefon": "123 45 678"
|
||||
},
|
||||
"icon": "user-plus",
|
||||
"labels": {
|
||||
"adresse": "Adresse",
|
||||
"epost": "E-post",
|
||||
"navn": "Navn",
|
||||
"telefon": "Telefon"
|
||||
},
|
||||
"road_description": "Meldingen går til kasserer, som registrerer deg og sender deg kontingenten på e-post. Når den er betalt er du medlem.",
|
||||
"road_icon": "lucide:coins",
|
||||
"road_title": "Etter innmelding",
|
||||
"who_description": "Kasserer i styret leser dette, og du kan nå oss på post@tomtervel.no i mellomtiden.",
|
||||
"who_icon": "lucide:user",
|
||||
"who_title": "Hvem svarer deg"
|
||||
},
|
||||
"endre-medlemskap": {
|
||||
"button": "Send endring",
|
||||
"description": "Har du flyttet, byttet e-post eller vil melde deg ut? Gi oss beskjed så vi har riktige opplysninger.",
|
||||
"encouragement": "Det tar bare et par minutter, og kasserer ordner resten.",
|
||||
"form_description": "Skriv navnet ditt og hva som er nytt.",
|
||||
"form_title": "Endre medlemskap",
|
||||
"heading": "Oppdater eller avslutt medlemskapet",
|
||||
"hints": {
|
||||
"adresse": "Ny adresse i Tomter",
|
||||
"hva_er_nytt": "Ny e-post, ny adresse, eller ønske om utmelding",
|
||||
"navn": "Ola Nordmann"
|
||||
},
|
||||
"icon": "pencil",
|
||||
"labels": {
|
||||
"adresse": "Adresse",
|
||||
"hva_er_nytt": "Hva vil du endre?",
|
||||
"navn": "Navn"
|
||||
},
|
||||
"road_description": "Kasserer får beskjeden og oppdaterer medlemslisten, eller melder deg ut om det er det du ber om.",
|
||||
"road_icon": "lucide:refresh-cw",
|
||||
"road_title": "Etter du sender inn",
|
||||
"who_description": "Kasserer i styret leser dette, og du kan nå oss på post@tomtervel.no i mellomtiden.",
|
||||
"who_icon": "lucide:user",
|
||||
"who_title": "Hvem svarer deg"
|
||||
}
|
||||
},
|
||||
"question": "Vil du bli medlem, eller endre medlemskapet ditt?"
|
||||
},
|
||||
"questions/melde-fra/index.yaml": {
|
||||
"description": "Har du sett noe farlig i trafikken, eller noe ødelagt på lekeplassen eller stien? Her sier du fra til velet.",
|
||||
"needs": {
|
||||
"anleggsfeil": {
|
||||
"button": "Send inn",
|
||||
"description": "Se noe ødelagt på en lekeplass eller langs elvestien, så tar lekeplasskomiteen tak i det.",
|
||||
"encouragement": "Det holder med noen ord om hva du så og hvor.",
|
||||
"form_description": "Fortell hva som er ødelagt og omtrent hvor det er.",
|
||||
"form_title": "Meld anleggsfeil",
|
||||
"heading": "Meld fra om noe ødelagt",
|
||||
"hints": {
|
||||
"hva": "Løs planke på gjerdet ved husken",
|
||||
"hvor": ""
|
||||
},
|
||||
"icon": "construction",
|
||||
"labels": {
|
||||
"hva": "Hva er ødelagt",
|
||||
"hvor": "Hvor er det"
|
||||
},
|
||||
"road_description": "Meldingen går til lekeplasskomiteen, som ser på den og tar tak i saken. Når feilen er utbedret, er saken avsluttet.",
|
||||
"road_icon": "lucide:wrench",
|
||||
"road_title": "Hva skjer videre",
|
||||
"who_description": "Lekeplasskomiteen leser meldingen din; til det haster kan du nå styret på post@tomtervel.no.",
|
||||
"who_icon": "lucide:hammer",
|
||||
"who_title": "Hvem leser dette"
|
||||
},
|
||||
"trafikkfare": {
|
||||
"button": "Send inn",
|
||||
"description": "Sett du noe farlig i trafikken? Fortell trafikkomiteen om det, så tar de det videre.",
|
||||
"encouragement": "Bare noen linjer om hva og hvor er nok til at vi kan følge opp.",
|
||||
"form_description": "Fortell hva du så, hvor det var, og legg gjerne igjen e-posten din hvis vi trenger å spørre mer.",
|
||||
"form_title": "Meld trafikkfare",
|
||||
"heading": "Meld fra om en trafikkfare",
|
||||
"hints": {
|
||||
"epost": "ola.nordmann@epost.no",
|
||||
"hva": "Dårlig sikt i krysset, biler kjører fort",
|
||||
"hvor": "Krysset Tomterveien / Elvestien"
|
||||
},
|
||||
"icon": "alert-triangle",
|
||||
"labels": {
|
||||
"epost": "E-post (valgfritt)",
|
||||
"hva": "Hva så du",
|
||||
"hvor": "Hvor var det"
|
||||
},
|
||||
"road_description": "Trafikkomiteen tar meldingen din opp med kommunen eller den som eier veien. Du får vite om saken blir ordnet eller avvist.",
|
||||
"road_icon": "lucide:route",
|
||||
"road_title": "Hva skjer videre",
|
||||
"who_description": "Trafikkomiteen leser meldingen din; haster det, kan du nå styret på post@tomtervel.no.",
|
||||
"who_icon": "lucide:car",
|
||||
"who_title": "Hvem leser dette"
|
||||
}
|
||||
},
|
||||
"question": "Hva vil du melde fra om?"
|
||||
},
|
||||
"questions/naboer/index.yaml": {
|
||||
"description": "Godt naboskap gjør hverdagen lettere. Trenger du en hånd, kan gi en, eller har noe å låne bort?",
|
||||
"needs": {
|
||||
"dele-utstyr": {
|
||||
"button": "Legg ut oppslag",
|
||||
"description": "Har du noe naboene kan få låne, eller trenger du å låne noe selv? Legg ut et oppslag så ser de andre på Tomter det.",
|
||||
"encouragement": "Vi legger det ut så naboene kan se det.",
|
||||
"form_description": "Skriv kort hva det gjelder, så folk skjønner det med en gang.",
|
||||
"form_title": "Lån eller utlån",
|
||||
"heading": "Lån ut eller lån av naboen",
|
||||
"hints": {
|
||||
"email": "ola@eksempel.no",
|
||||
"har_eller_trenger": "",
|
||||
"hva": "Hengerfeste til bil, tilhenger, stige",
|
||||
"navn": "Ola Nordmann"
|
||||
},
|
||||
"icon": "package",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"har_eller_trenger": "Har du eller trenger du",
|
||||
"hva": "Hva gjelder det",
|
||||
"navn": "Navn"
|
||||
},
|
||||
"road_description": "Oppslaget legges ut for naboene å se. Når det er ordnet eller ikke lenger aktuelt, tas det ned igjen.",
|
||||
"road_icon": "lucide:clipboard-list",
|
||||
"road_title": "Fra oppslag til avtale",
|
||||
"who_description": "Styret legger ut oppslaget og kan nås på post@tomtervel.no i mellomtiden.",
|
||||
"who_icon": "lucide:users",
|
||||
"who_title": "Hvem ser dette"
|
||||
},
|
||||
"kan-hjelpe": {
|
||||
"button": "Meld deg",
|
||||
"description": "Kan du stille opp med snømåking, en handletur eller skyss når en nabo trenger det? Meld deg, så blir du spurt når det passer.",
|
||||
"encouragement": "Du blir bare kontaktet når noe passer for deg.",
|
||||
"form_description": "Fortell kort hva du kan bidra med.",
|
||||
"form_title": "Meld deg som hjelper",
|
||||
"heading": "Bli spurt når en nabo trenger en hånd",
|
||||
"hints": {
|
||||
"email": "kari@eksempel.no",
|
||||
"kan": "",
|
||||
"navn": "Kari Nordmann",
|
||||
"telefon": "123 45 678"
|
||||
},
|
||||
"icon": "hand-helping",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"kan": "Hva kan du hjelpe med",
|
||||
"navn": "Navn",
|
||||
"telefon": "Telefon"
|
||||
},
|
||||
"road_description": "Du blir registrert som hjelper. Trekker du deg tilbake senere, er det bare å si fra.",
|
||||
"road_icon": "lucide:user-check",
|
||||
"road_title": "Fra påmelding til spørsmål",
|
||||
"who_description": "Nabohjelpen har oversikten og ringer deg direkte når det er noe, eller nås på post@tomtervel.no.",
|
||||
"who_icon": "lucide:heart-handshake",
|
||||
"who_title": "Hvem ser dette"
|
||||
},
|
||||
"trenger-hjelp": {
|
||||
"button": "Be om hjelp",
|
||||
"description": "Får du ikke til snømåking, en handletur eller skyss for tiden? Si fra, så finner vi en nabo som kan stille opp.",
|
||||
"encouragement": "Vi finner en nabo som kan stille opp så fort som mulig.",
|
||||
"form_description": "Skriv kort hva du trenger hjelp til.",
|
||||
"form_title": "Be om en hånd",
|
||||
"heading": "Få en nabo til å stille opp",
|
||||
"hints": {
|
||||
"adresse": "Tomterveien 12",
|
||||
"email": "kari@eksempel.no",
|
||||
"hva": "Måke snø på trappa, handle inn litt mat",
|
||||
"navn": "Kari Nordmann",
|
||||
"telefon": "123 45 678"
|
||||
},
|
||||
"icon": "heart-handshake",
|
||||
"labels": {
|
||||
"adresse": "Adresse",
|
||||
"email": "E-post",
|
||||
"hva": "Hva trenger du hjelp til",
|
||||
"navn": "Navn",
|
||||
"telefon": "Telefon"
|
||||
},
|
||||
"road_description": "Nabohjelpen finner en nabo som kan stille opp. Er det ordnet, eller om det ikke lot seg løse, får du beskjed.",
|
||||
"road_icon": "lucide:hand-helping",
|
||||
"road_title": "Fra melding til hjelp",
|
||||
"who_description": "Nabohjelpen tar imot og finner noen til deg, eller ring post@tomtervel.no i mellomtiden.",
|
||||
"who_icon": "lucide:users",
|
||||
"who_title": "Hvem ser dette"
|
||||
}
|
||||
},
|
||||
"question": "Trenger du en hånd, eller kan du gi en?"
|
||||
},
|
||||
"questions/nabohjelp/index.yaml": {
|
||||
"buckets": {
|
||||
"hjelpebehov": {
|
||||
"description": "Naboer som trenger en hånd med snømåking, handling eller skyss – klare til å bli koblet med noen som kan hjelpe.",
|
||||
"heading": "Naboer som trenger en hånd",
|
||||
"moves": {
|
||||
"formidlet->ikke_lost": "Meld ikke løst",
|
||||
"formidlet->ordnet": "Meld ordnet",
|
||||
"open->formidlet": "Koble til nabo",
|
||||
"open->ikke_lost": "Meld ikke løst"
|
||||
}
|
||||
},
|
||||
"hjelpere": {
|
||||
"description": "Naboer som har sagt de kan stille opp – klare til å bli spurt når noen trenger dem.",
|
||||
"heading": "Naboer som kan stille opp",
|
||||
"moves": {
|
||||
"open->registrert": "Registrer som hjelper",
|
||||
"open->trukket": "Meld trukket"
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirm": "Lagre",
|
||||
"description": "Her kobler du naboer som trenger hjelp med dem som kan stille opp.",
|
||||
"invited": {
|
||||
"description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.",
|
||||
"done": "Er inne",
|
||||
"heading": "Sendte invitasjoner"
|
||||
},
|
||||
"invites": {
|
||||
"nabohjelp": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i nabohjelpen, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i nabohjelpen",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {},
|
||||
"question": "Hvem trenger hjelp, og hvem kan stille opp?"
|
||||
},
|
||||
"questions/received/anleggsfeil.yaml": {
|
||||
"back": "Tilbake til forsiden",
|
||||
"description": "Meldingen din om skaden er sendt til lekeplasskomiteen. De ser over det du har sendt.",
|
||||
"next_description": "Lekeplasskomiteen ser på meldingen og tar tak i det som må fikses før noen skader seg.",
|
||||
"next_heading": "Komiteen tar tak i det",
|
||||
"question": "Hva skjer med meldingen min nå?",
|
||||
"see_all": {
|
||||
"button": "Se meldte feil",
|
||||
"description": "Se hva andre har meldt inn av skader på lekeplassene og stien, og hvor langt det har kommet.",
|
||||
"heading": "Se andre meldte feil"
|
||||
}
|
||||
},
|
||||
"questions/received/bli-medlem.yaml": {
|
||||
"back": "Tilbake til forsiden",
|
||||
"description": "Vi har mottatt innmeldingen din. Kasserer tar kontakt med betalingsinfo så snart medlemskapet er registrert.",
|
||||
"next_description": "Kasserer går gjennom nye innmeldinger og sender deg kontonummer og beløp for kontingenten.",
|
||||
"next_heading": "Kasserer sender deg betalingsinfo",
|
||||
"question": "Hva skjer etter at jeg har meldt meg inn?"
|
||||
},
|
||||
"questions/received/dele-utstyr.yaml": {
|
||||
"back": "Tilbake til forsiden",
|
||||
"description": "Takk for at du la ut noe naboene kan låne. Vi legger det ut på oppslaget så snart vi kan.",
|
||||
"next_description": "En i styret legger utstyret ut på naboenes oppslag, så andre kan se det og spørre om å låne.",
|
||||
"next_heading": "Styret legger det ut for deg",
|
||||
"question": "Hva skjer med det jeg har lagt ut?",
|
||||
"see_all": {
|
||||
"button": "Se hva naboer deler",
|
||||
"description": "Se hva andre i Tomter har lagt ut til utlån akkurat nå.",
|
||||
"heading": "Se alt naboene deler"
|
||||
}
|
||||
},
|
||||
"questions/received/endre-medlemskap.yaml": {
|
||||
"back": "Tilbake til forsiden",
|
||||
"description": "Vi har mottatt beskjeden om at du har flyttet, byttet e-post eller vil melde deg ut.",
|
||||
"next_description": "Kasserer oppdaterer medlemslista og gir deg beskjed hvis det er noe mer vi trenger fra deg.",
|
||||
"next_heading": "Kasserer retter opp opplysningene",
|
||||
"question": "Hva skjer nå med det jeg meldte inn?"
|
||||
},
|
||||
"questions/received/hjelpe-til.yaml": {
|
||||
"back": "Tilbake til forsiden",
|
||||
"description": "Takk for at du vil hjelpe til. Arrangementskomiteen har fått beskjed og tar kontakt med deg.",
|
||||
"next_description": "Du hører fra noen i komiteen når de vet hva de trenger hjelp til, gjerne noen dager før arrangementet eller dugnaden.",
|
||||
"next_heading": "Komiteen tar kontakt",
|
||||
"question": "Hva skjer nå som jeg har meldt meg?"
|
||||
},
|
||||
"questions/received/horingsinnspill.yaml": {
|
||||
"back": "Tilbake til forsiden",
|
||||
"description": "Innspillet ditt er sendt til høringsinstansen i velet.",
|
||||
"next_description": "De ser på om det kan tas med i velets høringssvar til kommunen, og gir beskjed om det blir med.",
|
||||
"next_heading": "Innspillet ditt inn i vurderingen",
|
||||
"question": "Hva skjer med det jeg har sagt om saken?"
|
||||
},
|
||||
"questions/received/kan-hjelpe.yaml": {
|
||||
"back": "Tilbake til forsiden",
|
||||
"description": "Takk for at du sier fra at du kan stille opp. Nabohjelp-komiteen tar imot meldingen og legger deg inn på lista over naboer som kan hjelpe.",
|
||||
"next_description": "Når en nabo trenger hjelp med noe du har sagt du kan bidra med, tar komiteen kontakt med deg direkte.",
|
||||
"next_heading": "Du blir kontaktet ved behov",
|
||||
"question": "Hva skjer nå som jeg har registrert meg?"
|
||||
},
|
||||
"questions/received/komite.yaml": {
|
||||
"back": "Tilbake til forsiden",
|
||||
"description": "Takk for at du vil bidra! Styret har fått beskjed og ser på hvilken komité som passer best for deg.",
|
||||
"next_description": "Noen fra styret tar kontakt for å høre mer om hva du liker å drive med, og finner en komité som passer.",
|
||||
"next_heading": "Vi finner riktig komité til deg",
|
||||
"question": "Hva skjer nå som jeg har meldt interesse for en komité?"
|
||||
},
|
||||
"questions/received/trafikkfare.yaml": {
|
||||
"back": "Tilbake til forsiden",
|
||||
"description": "Meldingen din om trafikkfaren er sendt til komiteen for trafikk.",
|
||||
"next_description": "Trafikkomiteen ser gjennom meldinger jevnlig og tar det videre med kommunen eller den som eier veien der det trengs.",
|
||||
"next_heading": "Trafikkomiteen tar den videre",
|
||||
"question": "Hva skjer med meldingen min nå?",
|
||||
"see_all": {
|
||||
"button": "Se andre trafikkfarer",
|
||||
"description": "Se hva andre har meldt inn, og status på sakene.",
|
||||
"heading": "Andre meldte trafikkfarer"
|
||||
}
|
||||
},
|
||||
"questions/received/trenger-hjelp.yaml": {
|
||||
"back": "Til forsiden",
|
||||
"description": "Meldingen din er sendt til nabohjelpa. Vi ser etter en nabo som kan stille opp.",
|
||||
"next_description": "Vi tar kontakt med noen som kan hjelpe, så snart vi finner en som passer.",
|
||||
"next_heading": "Vi finner en nabo til deg",
|
||||
"question": "Hva skjer nå som jeg har bedt om hjelp?"
|
||||
},
|
||||
"questions/status/anleggsfeil/index.yaml": {
|
||||
"back": "Tilbake til meldinger",
|
||||
"description": "Her ser du meldinger om ødelagt lekeutstyr og skader på elvestien, og hvor langt hver sak har kommet.",
|
||||
"heading": "Status på meldte anleggsfeil",
|
||||
"list_description": "Alle meldinger er åpne for alle å se.",
|
||||
"question": "Hva skjer med det jeg har meldt fra om?"
|
||||
},
|
||||
"questions/status/oppslag/index.yaml": {
|
||||
"back": "Tilbake",
|
||||
"description": "Her ser du alt som er sendt inn og hvor langt hvert oppslag har kommet.",
|
||||
"heading": "Oversikt over det som er meldt inn",
|
||||
"list_description": "Åpen liste – navn og detaljer vises ikke.",
|
||||
"question": "Hva er sendt inn, og hvor langt har det kommet?"
|
||||
},
|
||||
"questions/status/trafikkmeldinger/index.yaml": {
|
||||
"back": "Tilbake",
|
||||
"description": "Her ser du farlige steder i trafikken som er meldt inn, og hvor langt saken har kommet.",
|
||||
"heading": "Trafikkmeldinger og status",
|
||||
"list_description": "Alle meldinger som er sendt inn, og hvor de står nå.",
|
||||
"question": "Er det meldt fra om noe farlig i trafikken der jeg bor?"
|
||||
},
|
||||
"questions/styret/index.yaml": {
|
||||
"buckets": {
|
||||
"innspill": {
|
||||
"description": "Her samles meninger fra beboerne om saker kommunen har på høring, klare til å tas med i velets svar.",
|
||||
"heading": "Høringsinnspill fra beboerne",
|
||||
"moves": {}
|
||||
},
|
||||
"komitesoknader": {
|
||||
"description": "Her ligger alle som har meldt seg til en komité og venter på en plass.",
|
||||
"heading": "Søknader til komiteene",
|
||||
"moves": {
|
||||
"open->avslatt": "Avslå søknaden",
|
||||
"open->plassert": "Sett i komité"
|
||||
}
|
||||
},
|
||||
"oppslag": {
|
||||
"description": "Her ligger det naboer har å låne ut eller trenger å låne, klart til å legges ut.",
|
||||
"heading": "Utstyr til utlån blant naboer",
|
||||
"moves": {
|
||||
"open->lagt_ut": "Legg ut oppslaget",
|
||||
"open->tatt_ned": "Ta ned oppslaget"
|
||||
}
|
||||
},
|
||||
"trafikkmeldinger": {
|
||||
"description": "Her samles meldinger om farlige steder i trafikken, klare til å tas videre med kommunen eller veieier.",
|
||||
"heading": "Meldinger om trafikkfarer",
|
||||
"moves": {}
|
||||
}
|
||||
},
|
||||
"confirm": "Lagre",
|
||||
"description": "Herfra følger styret opp det som ikke hører under en enkelt komité: høringssvar, komitéplasser og oppslag fra naboer.",
|
||||
"invited": {
|
||||
"description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.",
|
||||
"done": "Er inne",
|
||||
"heading": "Sendte invitasjoner"
|
||||
},
|
||||
"invites": {
|
||||
"arrangementer": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i Arrangementer, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i Arrangementer",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
},
|
||||
"horingsinstans": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i Høringsinstans, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i Høringsinstans",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
},
|
||||
"kasserer": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i kassereren, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i kassereren",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
},
|
||||
"komiteer": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i komiteene, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i komiteene",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
},
|
||||
"lekeplasser": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i Leke- og møteplasser, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i Leke- og møteplasser",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
},
|
||||
"nabohjelp": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i nabohjelpen, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i nabohjelpen",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
},
|
||||
"styret": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i styret, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i styret",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
},
|
||||
"trafikk": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i Trafikkforhold, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i Trafikkforhold",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {},
|
||||
"question": "Hva venter på styret akkurat nå?"
|
||||
},
|
||||
"questions/trafikk/index.yaml": {
|
||||
"buckets": {
|
||||
"trafikkmeldinger": {
|
||||
"description": "Farlige steder i trafikken som naboer har meldt inn.",
|
||||
"heading": "Farlige steder i trafikken",
|
||||
"moves": {
|
||||
"open->avvist": "Avvis meldingen",
|
||||
"open->tatt_opp": "Ta opp med kommunen",
|
||||
"tatt_opp->avvist": "Avvis meldingen",
|
||||
"tatt_opp->ordnet": "Meld ordnet"
|
||||
}
|
||||
}
|
||||
},
|
||||
"confirm": "Lagre",
|
||||
"description": "Her ser du meldinger om farlige steder i trafikken og følger dem opp overfor kommunen eller veieier.",
|
||||
"invited": {
|
||||
"description": "Hver invitasjon med lenken som ble laget. Marker som ferdig når personen er inne.",
|
||||
"done": "Er inne",
|
||||
"heading": "Sendte invitasjoner"
|
||||
},
|
||||
"invites": {
|
||||
"trafikk": {
|
||||
"button": "Inviter",
|
||||
"description": "De får en konto, plass i Trafikkforhold, og en engangslenke der de selv velger passord eller passnøkkel. Send lenken videre til dem.",
|
||||
"form_title": "Hvem",
|
||||
"heading": "Inviter noen inn i Trafikkforhold",
|
||||
"labels": {
|
||||
"email": "E-post",
|
||||
"name": "Navn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {},
|
||||
"question": "Hvilke trafikkfarer må følges opp?"
|
||||
}
|
||||
}
|
||||
+237
-4
@@ -49,6 +49,14 @@ pub struct Plan {
|
||||
/// never disagree about what the committees are.
|
||||
#[serde(default)]
|
||||
pub groups: Vec<GroupDef>,
|
||||
/// Groups that may invite people into every group with a desk (the
|
||||
/// board), besides each group inviting into itself. An inviting
|
||||
/// group's desk gets one invite form per group it may fill (portal
|
||||
/// creates the account and a one-time credential link) and a list
|
||||
/// of the invites sent. This is onboarding from the content side:
|
||||
/// no admin session, a member of the business vouching for a person.
|
||||
#[serde(default)]
|
||||
pub invites_by: Vec<String>,
|
||||
pub needs: Vec<PlanNeed>,
|
||||
#[serde(default)]
|
||||
pub personas: Vec<Value>,
|
||||
@@ -63,6 +71,10 @@ fn english() -> String {
|
||||
pub struct GroupDef {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
/// Groups that may invite people into this one, besides itself and
|
||||
/// the plan's `invites_by`.
|
||||
#[serde(default)]
|
||||
pub invited_by: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -190,6 +202,11 @@ impl Plan {
|
||||
bail!("group {:?}: id must be unique ASCII lowercase with - or _, and it needs a label", g.id);
|
||||
}
|
||||
}
|
||||
for g in self.invites_by.iter().chain(self.groups.iter().flat_map(|g| g.invited_by.iter())) {
|
||||
if !groups(self).contains(g) {
|
||||
bail!("{g:?} may invite, but has no desk to invite from: it must handle, watch or move something");
|
||||
}
|
||||
}
|
||||
let mut seen = BTreeSet::new();
|
||||
for need in &self.needs {
|
||||
let at = |what: &str| format!("need {:?}: {what}", need.id);
|
||||
@@ -477,9 +494,29 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
||||
.filter(|n| n.enters_as.as_deref() == Some(group.as_str()))
|
||||
.map(|n| (n.id.clone(), json!({ "heading": n.wants, "description": "", "button": "Open" })))
|
||||
.collect();
|
||||
// One invite form per group this desk may fill, and the list of
|
||||
// invites sent, when it may fill any.
|
||||
let invites: Map<String, Value> = groups(plan)
|
||||
.iter()
|
||||
.filter(|g| inviters(plan, g).contains(&group))
|
||||
.map(|g| {
|
||||
(g.clone(), json!({
|
||||
"heading": format!("Invite someone into {}", label_of(plan, g)),
|
||||
"description": format!("They get an account, a place in {}, and a one-time link to set their own password or passkey.", label_of(plan, g)),
|
||||
"button": "Invite",
|
||||
"form_title": "Who",
|
||||
"labels": { "name": "Name", "email": "Email" },
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
let invited = if invites.is_empty() {
|
||||
Value::Null
|
||||
} else {
|
||||
json!({ "heading": "Invitations sent", "description": "Each invite with the link it made. Mark it done when the person is in.", "done": "Is in" })
|
||||
};
|
||||
words.insert(
|
||||
format!("questions/{group}/index.yaml"),
|
||||
json!({ "question": "What needs doing?", "description": "", "confirm": "Confirm", "buckets": buckets, "links": links }),
|
||||
json!({ "question": "What needs doing?", "description": "", "confirm": "Confirm", "buckets": buckets, "links": links, "invites": invites, "invited": invited }),
|
||||
);
|
||||
}
|
||||
// A public view per bucket the plan opens: a read-only list.
|
||||
@@ -567,6 +604,22 @@ fn groups(plan: &Plan) -> BTreeSet<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The groups whose desks carry an invite form into `group`: itself,
|
||||
/// the plan's `invites_by`, and its own `invited_by`, each of which
|
||||
/// must have a desk to carry it on.
|
||||
fn inviters(plan: &Plan, group: &str) -> BTreeSet<String> {
|
||||
let desks = groups(plan);
|
||||
std::iter::once(group.to_string())
|
||||
.chain(plan.invites_by.iter().cloned())
|
||||
.chain(plan.groups.iter().filter(|g| g.id == group).flat_map(|g| g.invited_by.iter().cloned()))
|
||||
.filter(|g| desks.contains(g))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn label_of(plan: &Plan, group: &str) -> String {
|
||||
plan.groups.iter().find(|g| g.id == group).map(|g| g.label.clone()).unwrap_or_else(|| humanize(group))
|
||||
}
|
||||
|
||||
/// Buckets the public may read, with the needs that fill them.
|
||||
fn public_buckets(plan: &Plan) -> Vec<(&str, Vec<&PlanNeed>)> {
|
||||
plan.by_bucket().into_iter().filter(|(_, needs)| needs.iter().any(|n| n.watched_by.iter().any(|g| g == PUBLIC))).collect()
|
||||
@@ -655,6 +708,11 @@ pub const ICONS: &[&str] = &[
|
||||
|
||||
/// `lucide:<name>` for a name on the list, otherwise nothing.
|
||||
pub fn icon_name(given: &str) -> Option<String> {
|
||||
// A lift token (see `lift_words`) has to reach the rendered file to
|
||||
// be matched against the repo's own icon.
|
||||
if given.starts_with('\u{1}') {
|
||||
return Some(given.to_string());
|
||||
}
|
||||
let name = given.trim().trim_start_matches("lucide:");
|
||||
ICONS.contains(&name).then(|| format!("lucide:{name}"))
|
||||
}
|
||||
@@ -861,6 +919,40 @@ pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BT
|
||||
"consequence": [s(&lw["button"])],
|
||||
}));
|
||||
}
|
||||
// Invites this desk may extend: `invite: {group}` is portal's
|
||||
// onboarding (an account, the group, a one-time credential
|
||||
// link), gated by this page's `qualifies`; what it made is
|
||||
// listed from the runtime's portal_invites bucket.
|
||||
if let Some(inv) = w["invites"].as_object().filter(|m| !m.is_empty()) {
|
||||
let mut targets: Vec<&String> = inv.keys().collect();
|
||||
targets.sort();
|
||||
for target in targets {
|
||||
let iw = &inv[target];
|
||||
alternatives.push(json!({
|
||||
"name": s(&iw["heading"]),
|
||||
"description": s(&iw["description"]),
|
||||
"action": format!("/{group}"),
|
||||
"consequence": [s(&iw["button"])],
|
||||
"invite": { "group": target },
|
||||
"features": [{ "name": s(&iw["form_title"]), "description": "", "requirements": [
|
||||
{ "name": "name", "label": s(&iw["labels"]["name"]) },
|
||||
{ "name": "email", "label": s(&iw["labels"]["email"]), "type": "email" },
|
||||
]}],
|
||||
}));
|
||||
}
|
||||
let lw = &w["invited"];
|
||||
alternatives.push(json!({
|
||||
"name": s(&lw["heading"]),
|
||||
"description": s(&lw["description"]),
|
||||
"action": format!("/{group}"),
|
||||
"consequence": [s(&w["confirm"])],
|
||||
"features": [{ "name": "", "description": "", "resource": {
|
||||
"source": { "kind": "kv", "bucket": "portal_invites" },
|
||||
"requires_group": group,
|
||||
"transitions": [{ "from": "open", "to": "done", "label": s(&lw["done"]) }],
|
||||
}}],
|
||||
}));
|
||||
}
|
||||
files.insert(format!("questions/{group}/_section.yaml"), json!({ "qualifies": group }));
|
||||
files.insert(
|
||||
format!("questions/{group}/index.yaml"),
|
||||
@@ -975,7 +1067,7 @@ fn words_prompt(plan: &Plan, path: &str, fallback: &Value) -> String {
|
||||
} else if plan.doors.iter().any(|d| door_path(d) == path) {
|
||||
"a page one kind of visitor reaches from the front door. `question` is what this page asks them; under `needs`, each entry is one alternative they can pick, with the form behind it."
|
||||
} else if path.ends_with("/index.yaml") {
|
||||
"a desk: the page one role inside the business works from. Under `buckets`, each entry is a list of cases; `moves` are the buttons that move a case from one stage to the next (key `from->to`), so label each with what the person has just done. Under `links` are pages this role can open."
|
||||
"a desk: the page one role inside the business works from. Under `buckets`, each entry is a list of cases; `moves` are the buttons that move a case from one stage to the next (key `from->to`), so label each with what the person has just done. Under `links` are pages this role can open. Under `invites`, each entry is a form that invites a person into that group - they get an account and a one-time link - and `invited` is the list of invites sent."
|
||||
} else if path.contains("/received/") || path.ends_with("-sent.yaml") {
|
||||
"a page shown right after someone has sent a form. Say who reads it, when they answer, and what happens next."
|
||||
} else {
|
||||
@@ -1006,14 +1098,33 @@ fn words_prompt(plan: &Plan, path: &str, fallback: &Value) -> String {
|
||||
|
||||
pub fn run(argv: &[String]) -> anyhow::Result<()> {
|
||||
let value = |flag: &str| argv.iter().position(|a| a == flag).and_then(|i| argv.get(i + 1)).cloned();
|
||||
let usage = "usage: portal-trainer build --plan <plan.yaml> --out <dir> [--llm 'cmd'] [--lint question_lint]";
|
||||
let usage = "usage: portal-trainer build --plan <plan.yaml> --out <dir> [--words-from <existing repo>] [--words <overrides.json>] [--llm 'cmd'] [--lint question_lint]";
|
||||
let plan_path = value("--plan").ok_or_else(|| anyhow!(usage))?;
|
||||
let out = std::path::PathBuf::from(value("--out").ok_or_else(|| anyhow!(usage))?);
|
||||
let plan: Plan = serde_yaml::from_str(&std::fs::read_to_string(&plan_path).with_context(|| format!("reading {plan_path}"))?)
|
||||
.with_context(|| format!("parsing {plan_path}"))?;
|
||||
plan.validate()?;
|
||||
|
||||
let mut words = fallback_words(&plan);
|
||||
// Words, in layers: the fallbacks; what an existing repo already
|
||||
// says (hand edits survive a rebuild); explicit overrides for new
|
||||
// slots; and only then a model, for whatever is still fallback.
|
||||
let mut words = match value("--words-from") {
|
||||
Some(repo) => lift_words(&plan, Path::new(&repo))?,
|
||||
None => fallback_words(&plan),
|
||||
};
|
||||
if let Some(path) = value("--words") {
|
||||
let given: BTreeMap<String, Value> = serde_json::from_str(&std::fs::read_to_string(&path).with_context(|| format!("reading {path}"))?)
|
||||
.with_context(|| format!("parsing {path}"))?;
|
||||
for (page, v) in given {
|
||||
match words.get(&page) {
|
||||
Some(base) => {
|
||||
let merged = merge_words(base, &v);
|
||||
words.insert(page, merged);
|
||||
}
|
||||
None => eprintln!("words: {page} is not a page of this plan, ignored"),
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(cmd) = value("--llm") {
|
||||
std::fs::create_dir_all(out.join("llm"))?;
|
||||
for (n, (path, fallback)) in words.clone().iter().enumerate() {
|
||||
@@ -1030,6 +1141,8 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
write_repo(&out, render(&plan, &words)?)?;
|
||||
// The words as used, so the next rebuild can start from them.
|
||||
std::fs::write(out.join("words.json"), serde_json::to_string_pretty(&words)?)?;
|
||||
println!("content repo written to {}", out.display());
|
||||
|
||||
if let Some(lint) = value("--lint") {
|
||||
@@ -1297,3 +1410,123 @@ mod tests {
|
||||
assert!(plan.validate().unwrap_err().to_string().contains("must agree"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The words a repo already carries, lifted back into the words map by
|
||||
/// structure: the plan is rendered with a token in every word slot,
|
||||
/// each rendered file is walked alongside the repo's file of the same
|
||||
/// path, and wherever a token meets a string, that string is the word.
|
||||
/// Pages the repo does not have, and slots it never had (a new invite
|
||||
/// form), keep their fallback. Hand-edited wording survives a rebuild.
|
||||
pub fn lift_words(plan: &Plan, repo: &Path) -> anyhow::Result<BTreeMap<String, Value>> {
|
||||
let fallback = fallback_words(plan);
|
||||
let tokened: BTreeMap<String, Value> =
|
||||
fallback.iter().map(|(path, v)| (path.clone(), tokenize(v, path, ""))).collect();
|
||||
let rendered = render(plan, &tokened)?;
|
||||
let mut found: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||
for (path, text) in &rendered {
|
||||
let Ok(live) = std::fs::read_to_string(repo.join(path)) else { continue };
|
||||
let (Ok(a), Ok(b)) = (
|
||||
serde_yaml::from_str::<serde_yaml::Value>(text),
|
||||
serde_yaml::from_str::<serde_yaml::Value>(&live),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
collect_words(&a, &b, &mut found);
|
||||
}
|
||||
Ok(fallback.iter().map(|(path, v)| (path.clone(), untokenize(v, path, "", &found))).collect())
|
||||
}
|
||||
|
||||
fn token(path: &str, pointer: &str) -> String {
|
||||
format!("\u{1}{path}\u{2}{pointer}\u{3}")
|
||||
}
|
||||
|
||||
fn tokenize(v: &Value, path: &str, pointer: &str) -> Value {
|
||||
match v {
|
||||
Value::Object(m) => Value::Object(m.iter().map(|(k, x)| (k.clone(), tokenize(x, path, &format!("{pointer}/{k}")))).collect()),
|
||||
Value::String(_) => Value::String(token(path, pointer)),
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn untokenize(v: &Value, path: &str, pointer: &str, found: &std::collections::HashMap<String, String>) -> Value {
|
||||
match v {
|
||||
Value::Object(m) => Value::Object(m.iter().map(|(k, x)| (k.clone(), untokenize(x, path, &format!("{pointer}/{k}"), found))).collect()),
|
||||
Value::String(_) => match found.get(&token(path, pointer)) {
|
||||
Some(word) => Value::String(word.clone()),
|
||||
None => v.clone(),
|
||||
},
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_words(a: &serde_yaml::Value, b: &serde_yaml::Value, found: &mut std::collections::HashMap<String, String>) {
|
||||
match (a, b) {
|
||||
(serde_yaml::Value::Mapping(x), serde_yaml::Value::Mapping(y)) => {
|
||||
for (k, va) in x {
|
||||
if let Some(vb) = y.get(k) {
|
||||
collect_words(va, vb, found);
|
||||
}
|
||||
}
|
||||
}
|
||||
(serde_yaml::Value::Sequence(x), serde_yaml::Value::Sequence(y)) => {
|
||||
for (va, vb) in x.iter().zip(y) {
|
||||
collect_words(va, vb, found);
|
||||
}
|
||||
}
|
||||
(serde_yaml::Value::String(t), serde_yaml::Value::String(live)) if t.starts_with('\u{1}') => {
|
||||
// An icon slot holds the bare name; the file carries `lucide:`.
|
||||
let word = if t.ends_with("/icon\u{3}") { live.trim_start_matches("lucide:") } else { live.as_str() };
|
||||
found.insert(t.clone(), word.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod invite_tests {
|
||||
use super::*;
|
||||
|
||||
const PLAN: &str = "business: Vel\ncontact: { name: S, email: s@v.example }\ngroups:\n - { id: trafikk, label: Trafikk }\ninvites_by: [styret]\nneeds:\n - id: melde\n who: a neighbour\n wants: a hazard seen to\n lands_in: farer\n handled_by: trafikk\n stages: [open, ordnet]\n done_when: [ordnet]\n max_fields: 3\n collects:\n - { name: hvor }\n - id: utlegg\n who: a committee member\n wants: money back\n enters_as: trafikk\n lands_in: utlegg\n handled_by: styret\n stages: [open, betalt, avvist]\n done_when: [betalt, avvist]\n max_fields: 3\n collects:\n - { name: belop }\n";
|
||||
|
||||
#[test]
|
||||
fn inviting_desks_get_a_form_per_group_and_a_list_of_invites_sent() {
|
||||
let plan: Plan = serde_yaml::from_str(PLAN).unwrap();
|
||||
plan.validate().unwrap();
|
||||
let files = render(&plan, &fallback_words(&plan)).unwrap();
|
||||
// The board invites into both groups; trafikk only into itself.
|
||||
let styret = &files["questions/styret/index.yaml"];
|
||||
assert_eq!(styret.matches("invite:").count(), 2, "{styret}");
|
||||
assert!(styret.contains("bucket: portal_invites"));
|
||||
let trafikk = &files["questions/trafikk/index.yaml"];
|
||||
assert_eq!(trafikk.matches("invite:").count(), 1);
|
||||
assert!(trafikk.contains("group: trafikk") && !trafikk.contains("group: styret"));
|
||||
// The plan is refused when an inviter has no desk.
|
||||
let bad: Plan = serde_yaml::from_str(&PLAN.replace("invites_by: [styret]", "invites_by: [nobody]")).unwrap();
|
||||
assert!(bad.validate().unwrap_err().to_string().contains("no desk"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn words_lift_back_out_of_a_rendered_repo_and_new_slots_keep_their_fallback() {
|
||||
let plan: Plan = serde_yaml::from_str(PLAN).unwrap();
|
||||
let mut words = fallback_words(&plan);
|
||||
// A hand-edited desk: Norwegian words in the live repo.
|
||||
let desk = words.get_mut("questions/trafikk/index.yaml").unwrap();
|
||||
desk["question"] = json!("Hva må gjøres?");
|
||||
desk["buckets"]["farer"]["heading"] = json!("Meldte farer");
|
||||
desk["buckets"]["farer"]["moves"]["open->ordnet"] = json!("Ordnet");
|
||||
let dir = std::env::temp_dir().join(format!("lift-{}", std::process::id()));
|
||||
write_repo(&dir, render(&plan, &words).unwrap()).unwrap();
|
||||
|
||||
let lifted = lift_words(&plan, &dir).unwrap();
|
||||
let desk = &lifted["questions/trafikk/index.yaml"];
|
||||
assert_eq!(desk["question"], "Hva må gjøres?");
|
||||
assert_eq!(desk["buckets"]["farer"]["heading"], "Meldte farer");
|
||||
assert_eq!(desk["buckets"]["farer"]["moves"]["open->ordnet"], "Ordnet");
|
||||
// A slot the repo never had keeps the fallback.
|
||||
let plan2: Plan = serde_yaml::from_str(&PLAN.replace("invites_by: [styret]", "invites_by: [styret, trafikk]")).unwrap();
|
||||
let lifted2 = lift_words(&plan2, &dir).unwrap();
|
||||
assert_eq!(lifted2["questions/trafikk/index.yaml"]["question"], "Hva må gjøres?");
|
||||
assert!(lifted2["questions/trafikk/index.yaml"]["invites"]["styret"]["heading"].as_str().unwrap().starts_with("Invite someone"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user